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.ArrayDeque;
23 import java.util.Collections;
24 import java.util.Deque;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Iterator;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Optional;
32 import java.util.Set;
33
34 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
35 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
36 import com.puppycrawl.tools.checkstyle.api.DetailAST;
37 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
38 import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
39 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
40 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
41
42 /**
43 * <div>
44 * Checks that a local variable is declared and/or assigned, but not used.
45 * Supports
46 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.30">
47 * pattern variables</a>.
48 * Doesn't check
49 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.12.3">
50 * array components</a> as array
51 * components are classified as different kind of variables by
52 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/index.html">JLS</a>.
53 * </div>
54 *
55 * @since 9.3
56 */
57 @FileStatefulCheck
58 public class UnusedLocalVariableCheck extends AbstractCheck {
59
60 /**
61 * A key is pointing to the warning message text in "messages.properties"
62 * file.
63 */
64 public static final String MSG_UNUSED_LOCAL_VARIABLE = "unused.local.var";
65
66 /**
67 * A key is pointing to the warning message text in "messages.properties"
68 * file.
69 */
70 public static final String MSG_UNUSED_NAMED_LOCAL_VARIABLE = "unused.named.local.var";
71
72 /**
73 * An array of increment and decrement tokens.
74 */
75 private static final int[] INCREMENT_AND_DECREMENT_TOKENS = {
76 TokenTypes.POST_INC,
77 TokenTypes.POST_DEC,
78 TokenTypes.INC,
79 TokenTypes.DEC,
80 };
81
82 /**
83 * An array of scope tokens.
84 */
85 private static final int[] SCOPES = {
86 TokenTypes.SLIST,
87 TokenTypes.LITERAL_FOR,
88 TokenTypes.OBJBLOCK,
89 };
90
91 /**
92 * An array of unacceptable children of ast of type {@link TokenTypes#DOT}.
93 */
94 private static final int[] UNACCEPTABLE_CHILD_OF_DOT = {
95 TokenTypes.DOT,
96 TokenTypes.METHOD_CALL,
97 TokenTypes.LITERAL_NEW,
98 TokenTypes.LITERAL_SUPER,
99 TokenTypes.LITERAL_CLASS,
100 TokenTypes.LITERAL_THIS,
101 };
102
103 /**
104 * An array of unacceptable parent of ast of type {@link TokenTypes#IDENT}.
105 */
106 private static final int[] UNACCEPTABLE_PARENT_OF_IDENT = {
107 TokenTypes.VARIABLE_DEF,
108 TokenTypes.DOT,
109 TokenTypes.LITERAL_NEW,
110 TokenTypes.PATTERN_VARIABLE_DEF,
111 TokenTypes.METHOD_CALL,
112 TokenTypes.TYPE,
113 };
114
115 /**
116 * An array of blocks in which local anon inner classes can exist.
117 */
118 private static final int[] ANONYMOUS_CLASS_PARENT_TOKENS = {
119 TokenTypes.METHOD_DEF,
120 TokenTypes.CTOR_DEF,
121 TokenTypes.STATIC_INIT,
122 TokenTypes.INSTANCE_INIT,
123 TokenTypes.COMPACT_CTOR_DEF,
124 };
125
126 /**
127 * An array of token types that indicate a variable is being used within
128 * an expression involving increment or decrement operators, or within a switch statement.
129 * When a token of one of these types is the parent of an expression, it indicates that the
130 * variable associated with the increment or decrement operation is being used.
131 * Ex:- TokenTypes.LITERAL_SWITCH: Indicates a switch statement. Variables used within the
132 * switch expression are considered to be used
133 */
134 private static final int[] INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES = {
135 TokenTypes.ELIST,
136 TokenTypes.INDEX_OP,
137 TokenTypes.ASSIGN,
138 TokenTypes.LITERAL_SWITCH,
139 };
140
141 /** Package separator. */
142 private static final String PACKAGE_SEPARATOR = ".";
143
144 /**
145 * Symbol used to represent unnamed variables in Java pattern matching.
146 */
147 private static final String UNNAMED_VAR = "_";
148
149 /**
150 * Constant for JDK 22 version number.
151 */
152 private static final int JDK_22 = 22;
153
154 /**
155 * Keeps tracks of the variables declared in file.
156 */
157 private final Deque<VariableDesc> variables = new ArrayDeque<>();
158
159 /**
160 * Keeps track of all the type declarations present in the file.
161 * Pops the type out of the stack while leaving the type
162 * in visitor pattern.
163 */
164 private final Deque<TypeDeclDesc> typeDeclarations = new ArrayDeque<>();
165
166 /**
167 * Maps type declaration ast to their respective TypeDeclDesc objects.
168 */
169 private final Map<DetailAST, TypeDeclDesc> typeDeclAstToTypeDeclDesc = new LinkedHashMap<>();
170
171 /**
172 * Maps local anonymous inner class to the TypeDeclDesc object
173 * containing it.
174 */
175 private final Map<DetailAST, TypeDeclDesc> anonInnerAstToTypeDeclDesc = new HashMap<>();
176
177 /**
178 * Set of tokens of type {@link UnusedLocalVariableCheck#ANONYMOUS_CLASS_PARENT_TOKENS}
179 * and {@link TokenTypes#LAMBDA} in some cases.
180 */
181 private final Set<DetailAST> anonInnerClassHolders = new HashSet<>();
182
183 /**
184 * Allow variables named with a single underscore
185 * (known as <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
186 * unnamed variables</a> in Java 21+).
187 */
188 private boolean allowUnnamedVariables = true;
189
190 /**
191 * Set the JDK version that you are using.
192 * Old JDK version numbering is supported (e.g. 1.8 for Java 8)
193 * as well as just the major JDK version alone (e.g. 8) is supported.
194 * This property only considers features from officially released
195 * Java versions as supported. Features introduced in preview releases
196 * are not considered supported until they are included in a non-preview release.
197 * Before JDK 22, named pattern variables in switch labels and instanceof
198 * record Destructuring cannot be replaced with {@code _}, so violations
199 * on them are suppressed when jdkVersion is set below 22.
200 */
201 private int jdkVersion = JDK_22;
202
203 /**
204 * Name of the package.
205 */
206 private String packageName;
207
208 /**
209 * Depth at which a type declaration is nested, 0 for top level type declarations.
210 */
211 private int depth;
212
213 /**
214 * Setter to allow variables named with a single underscore
215 * (known as <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
216 * unnamed variables</a> in Java 21+).
217 *
218 * @param allowUnnamedVariables true or false.
219 * @since 10.18.0
220 */
221 public void setAllowUnnamedVariables(boolean allowUnnamedVariables) {
222 this.allowUnnamedVariables = allowUnnamedVariables;
223 }
224
225 /**
226 * Setter to set the JDK version that you are using.
227 * Old JDK version numbering is supported (e.g. 1.8 for Java 8)
228 * as well as just the major JDK version alone (e.g. 8) is supported.
229 * This property only considers features from officially released
230 * Java versions as supported. Features introduced in preview releases
231 * are not considered supported until they are included in a non-preview release.
232 * Before JDK 22, named pattern variables in switch labels and instanceof
233 * record Destructuring cannot be replaced with {@code _}, so violations
234 * on them are suppressed when jdkVersion is set below 22.
235 *
236 * @param jdkVersion the Java version.
237 * @since 13.7.0
238 */
239 public void setJdkVersion(String jdkVersion) {
240 final String singleVersionNumber;
241 if (jdkVersion.startsWith("1.")) {
242 singleVersionNumber = jdkVersion.substring(2);
243 }
244 else {
245 singleVersionNumber = jdkVersion;
246 }
247 this.jdkVersion = Integer.parseInt(singleVersionNumber);
248 }
249
250 @Override
251 public int[] getDefaultTokens() {
252 return new int[] {
253 TokenTypes.DOT,
254 TokenTypes.VARIABLE_DEF,
255 TokenTypes.IDENT,
256 TokenTypes.SLIST,
257 TokenTypes.LITERAL_FOR,
258 TokenTypes.OBJBLOCK,
259 TokenTypes.CLASS_DEF,
260 TokenTypes.INTERFACE_DEF,
261 TokenTypes.ANNOTATION_DEF,
262 TokenTypes.PACKAGE_DEF,
263 TokenTypes.LITERAL_NEW,
264 TokenTypes.METHOD_DEF,
265 TokenTypes.CTOR_DEF,
266 TokenTypes.STATIC_INIT,
267 TokenTypes.INSTANCE_INIT,
268 TokenTypes.COMPILATION_UNIT,
269 TokenTypes.LAMBDA,
270 TokenTypes.ENUM_DEF,
271 TokenTypes.RECORD_DEF,
272 TokenTypes.COMPACT_CTOR_DEF,
273 TokenTypes.PATTERN_VARIABLE_DEF,
274 };
275 }
276
277 @Override
278 public int[] getAcceptableTokens() {
279 return getDefaultTokens();
280 }
281
282 @Override
283 public int[] getRequiredTokens() {
284 return getDefaultTokens();
285 }
286
287 @Override
288 public void beginTree(DetailAST root) {
289 variables.clear();
290 typeDeclarations.clear();
291 typeDeclAstToTypeDeclDesc.clear();
292 anonInnerAstToTypeDeclDesc.clear();
293 anonInnerClassHolders.clear();
294 packageName = null;
295 depth = 0;
296 }
297
298 @Override
299 public void visitToken(DetailAST ast) {
300 final int type = ast.getType();
301 if (type == TokenTypes.DOT) {
302 visitDotToken(ast, variables);
303 }
304 else if (type == TokenTypes.VARIABLE_DEF && !skipUnnamedVariables(ast)) {
305 visitVariableDefToken(ast);
306 }
307 else if (type == TokenTypes.PATTERN_VARIABLE_DEF
308 && !skipUnnamedPatternVariables(ast)) {
309 addPatternVariable(ast, variables);
310 }
311 else if (type == TokenTypes.IDENT) {
312 visitIdentToken(ast, variables);
313 }
314 else if (isInsideLocalAnonInnerClass(ast)) {
315 visitLocalAnonInnerClass(ast);
316 }
317 else if (isNonLocalTypeDeclaration(ast)) {
318 visitNonLocalTypeDeclarationToken(ast);
319 }
320 else if (type == TokenTypes.PACKAGE_DEF) {
321 packageName = CheckUtil.extractQualifiedName(ast.getFirstChild().getNextSibling());
322 }
323 }
324
325 @Override
326 public void leaveToken(DetailAST ast) {
327 if (TokenUtil.isOfType(ast, SCOPES)) {
328 logViolations(ast, variables);
329 }
330 else if (ast.getType() == TokenTypes.COMPILATION_UNIT) {
331 leaveCompilationUnit();
332 }
333 else if (isNonLocalTypeDeclaration(ast)) {
334 depth--;
335 typeDeclarations.pop();
336 }
337 }
338
339 /**
340 * Visit ast of type {@link TokenTypes#DOT}.
341 *
342 * @param dotAst dotAst
343 * @param variablesStack stack of all the relevant variables in the scope
344 */
345 private static void visitDotToken(DetailAST dotAst, Deque<VariableDesc> variablesStack) {
346 if (dotAst.getParent().getType() != TokenTypes.LITERAL_NEW
347 && shouldCheckIdentTokenNestedUnderDot(dotAst)) {
348 final DetailAST identifier = dotAst.findFirstToken(TokenTypes.IDENT);
349 if (identifier != null) {
350 checkIdentifierAst(identifier, variablesStack);
351 }
352 }
353 }
354
355 /**
356 * Visit ast of type {@link TokenTypes#VARIABLE_DEF}.
357 *
358 * @param varDefAst varDefAst
359 */
360 private void visitVariableDefToken(DetailAST varDefAst) {
361 addLocalVariables(varDefAst, variables);
362 addInstanceOrClassVar(varDefAst);
363 }
364
365 /**
366 * Visit ast of type {@link TokenTypes#IDENT}.
367 *
368 * @param identAst identAst
369 * @param variablesStack stack of all the relevant variables in the scope
370 */
371 private static void visitIdentToken(DetailAST identAst, Deque<VariableDesc> variablesStack) {
372 final DetailAST parent = identAst.getParent();
373 final boolean isMethodReferenceMethodName = parent.getType() == TokenTypes.METHOD_REF
374 && parent.getFirstChild() != identAst;
375 final boolean isConstructorReference = parent.getType() == TokenTypes.METHOD_REF
376 && parent.getLastChild().getType() == TokenTypes.LITERAL_NEW;
377 final boolean isNestedClassInitialization =
378 TokenUtil.isOfType(identAst.getNextSibling(), TokenTypes.LITERAL_NEW)
379 && parent.getType() == TokenTypes.DOT;
380
381 if (isNestedClassInitialization || !isMethodReferenceMethodName
382 && !isConstructorReference
383 && !TokenUtil.isOfType(parent, UNACCEPTABLE_PARENT_OF_IDENT)) {
384 checkIdentifierAst(identAst, variablesStack);
385 }
386 }
387
388 /**
389 * Visit the non-local type declaration token.
390 *
391 * @param typeDeclAst type declaration ast
392 */
393 private void visitNonLocalTypeDeclarationToken(DetailAST typeDeclAst) {
394 final String qualifiedName = getQualifiedTypeDeclarationName(typeDeclAst);
395 final TypeDeclDesc currTypeDecl = new TypeDeclDesc(qualifiedName, depth, typeDeclAst);
396 depth++;
397 typeDeclarations.push(currTypeDecl);
398 typeDeclAstToTypeDeclDesc.put(typeDeclAst, currTypeDecl);
399 }
400
401 /**
402 * Visit the local anon inner class.
403 *
404 * @param literalNewAst literalNewAst
405 */
406 private void visitLocalAnonInnerClass(DetailAST literalNewAst) {
407 anonInnerAstToTypeDeclDesc.put(literalNewAst, typeDeclarations.peek());
408 anonInnerClassHolders.add(getBlockContainingLocalAnonInnerClass(literalNewAst));
409 }
410
411 /**
412 * Check for skip current {@link TokenTypes#VARIABLE_DEF}
413 * due to <b>allowUnnamedVariable</b> option.
414 *
415 * @param varDefAst varDefAst variable to check
416 * @return true if the current variable should be skipped.
417 */
418 private boolean skipUnnamedVariables(DetailAST varDefAst) {
419 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
420 return allowUnnamedVariables && UNNAMED_VAR.equals(ident.getText());
421 }
422
423 /**
424 * Checks whether the specified current pattern variable is an unnamed pattern variable.
425 *
426 * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
427 * @return true if the current pattern variable should be skipped.
428 */
429 private static boolean skipUnnamedPatternVariables(DetailAST patternVarDefAst) {
430 final DetailAST ident = patternVarDefAst.findFirstToken(TokenTypes.IDENT);
431 return UNNAMED_VAR.equals(ident.getText());
432 }
433
434 /**
435 * Add a pattern variable to the {@code variablesStack} stack.
436 *
437 * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
438 * @param variablesStack stack of all the relevant variables in the scope
439 */
440 private static void addPatternVariable(DetailAST patternVarDefAst,
441 Deque<VariableDesc> variablesStack) {
442 final DetailAST ident = patternVarDefAst.findFirstToken(TokenTypes.IDENT);
443 final DetailAST scope = findScopeOfPatternVariable(patternVarDefAst);
444 final VariableDesc desc = new VariableDesc(ident.getText(), ident, scope);
445 if (isForcedNamePatternVariable(patternVarDefAst)) {
446 desc.registerAsNamedPatternVar();
447 }
448 variablesStack.push(desc);
449 }
450
451 /**
452 * Checks whether the pattern variable is declared in a switch labels and instanceof.
453 *
454 * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
455 * @return true if the pattern variable is in a forced-name context
456 */
457 private static boolean isForcedNamePatternVariable(DetailAST patternVarDefAst) {
458 return patternVarDefAst.getParent().getType() != TokenTypes.LITERAL_INSTANCEOF;
459 }
460
461 /**
462 * Find the scope of a pattern variable.
463 *
464 * @param patternVarDefAst ast of type.
465 * @return the outermost enclosing {@link TokenTypes#SLIST}, or {@code null} if none.
466 */
467 private static DetailAST findScopeOfPatternVariable(DetailAST patternVarDefAst) {
468 final Deque<DetailAST> slistAncestors = new ArrayDeque<>();
469 for (DetailAST current = patternVarDefAst;
470 current != null;
471 current = current.getParent()) {
472 if (current.getType() == TokenTypes.SLIST) {
473 slistAncestors.push(current);
474 }
475 }
476 return slistAncestors.peekLast();
477 }
478
479 /**
480 * Whether ast node of type {@link TokenTypes#LITERAL_NEW} is a part of a local
481 * anonymous inner class.
482 *
483 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
484 * @return true if variableDefAst is an instance variable in local anonymous inner class
485 */
486 private static boolean isInsideLocalAnonInnerClass(DetailAST literalNewAst) {
487 boolean result = false;
488 final DetailAST lastChild = literalNewAst.getLastChild();
489 if (lastChild != null && lastChild.getType() == TokenTypes.OBJBLOCK) {
490 DetailAST currentAst = literalNewAst;
491 while (currentAst != null
492 && !TokenUtil.isTypeDeclaration(currentAst.getType())) {
493 if (currentAst.getType() == TokenTypes.SLIST) {
494 result = true;
495 break;
496 }
497 currentAst = currentAst.getParent();
498 }
499 }
500 return result;
501 }
502
503 /**
504 * Traverse {@code variablesStack} stack and log the violations.
505 *
506 * @param scopeAst ast node of type {@link UnusedLocalVariableCheck#SCOPES}
507 * @param variablesStack stack of all the relevant variables in the scope
508 */
509 private void logViolations(DetailAST scopeAst, Deque<VariableDesc> variablesStack) {
510 final Iterator<VariableDesc> iterator = variablesStack.iterator();
511 while (iterator.hasNext()) {
512 final VariableDesc variableDesc = iterator.next();
513 if (variableDesc.getScope() == scopeAst) {
514 iterator.remove();
515 if (!variableDesc.isUsed()
516 && !variableDesc.isInstVarOrClassVar()
517 && !(jdkVersion < JDK_22
518 && variableDesc.isNamedPatternVar())) {
519 final DetailAST typeAst = variableDesc.getTypeAst();
520 if (allowUnnamedVariables) {
521 log(typeAst, MSG_UNUSED_NAMED_LOCAL_VARIABLE, variableDesc.getName());
522 }
523 else {
524 log(typeAst, MSG_UNUSED_LOCAL_VARIABLE, variableDesc.getName());
525 }
526 }
527 }
528 }
529 }
530
531 /**
532 * We process all the blocks containing local anonymous inner classes
533 * separately after processing all the other nodes. This is being done
534 * due to the fact the instance variables of local anon inner classes can
535 * cast a shadow on local variables.
536 */
537 private void leaveCompilationUnit() {
538 anonInnerClassHolders.forEach(holder -> {
539 iterateOverBlockContainingLocalAnonInnerClass(holder, new ArrayDeque<>());
540 });
541 }
542
543 /**
544 * Whether a type declaration is non-local. Annotated interfaces are always non-local.
545 *
546 * @param typeDeclAst type declaration ast
547 * @return true if type declaration is non-local
548 */
549 private static boolean isNonLocalTypeDeclaration(DetailAST typeDeclAst) {
550 return TokenUtil.isTypeDeclaration(typeDeclAst.getType())
551 && typeDeclAst.getParent().getType() != TokenTypes.SLIST;
552 }
553
554 /**
555 * Get the block containing local anon inner class.
556 *
557 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
558 * @return the block containing local anon inner class
559 */
560 private static DetailAST getBlockContainingLocalAnonInnerClass(DetailAST literalNewAst) {
561 DetailAST currentAst = literalNewAst;
562 DetailAST result = null;
563 DetailAST topMostLambdaAst = null;
564 boolean continueSearch = true;
565 while (continueSearch) {
566 continueSearch = false;
567 while (currentAst != null
568 && !TokenUtil.isOfType(currentAst, ANONYMOUS_CLASS_PARENT_TOKENS)) {
569 if (currentAst.getType() == TokenTypes.LAMBDA) {
570 topMostLambdaAst = currentAst;
571 currentAst = currentAst.getParent();
572 continueSearch = true;
573 break;
574 }
575 currentAst = currentAst.getParent();
576 result = currentAst;
577 }
578 }
579
580 if (currentAst == null) {
581 result = topMostLambdaAst;
582 }
583 return result;
584 }
585
586 /**
587 * Add local variables to the {@code variablesStack} stack.
588 * Also adds the instance variables defined in a local anonymous inner class.
589 *
590 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
591 * @param variablesStack stack of all the relevant variables in the scope
592 */
593 private static void addLocalVariables(DetailAST varDefAst, Deque<VariableDesc> variablesStack) {
594 final DetailAST parentAst = varDefAst.getParent();
595 final DetailAST grandParent = parentAst.getParent();
596
597 if (grandParent != null) {
598 final boolean isInstanceVarInInnerClass =
599 grandParent.getType() == TokenTypes.LITERAL_NEW
600 || grandParent.getType() == TokenTypes.CLASS_DEF;
601 if (isInstanceVarInInnerClass
602 || parentAst.getType() != TokenTypes.OBJBLOCK) {
603 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
604 final VariableDesc desc = new VariableDesc(ident.getText(),
605 varDefAst.findFirstToken(TokenTypes.TYPE), findScopeOfVariable(varDefAst));
606 if (isInstanceVarInInnerClass) {
607 desc.registerAsInstOrClassVar();
608 }
609 variablesStack.push(desc);
610 }
611 }
612 }
613
614 /**
615 * Add instance variables and class variables to the
616 * {@link TypeDeclDesc#instanceAndClassVarStack}.
617 *
618 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
619 */
620 private void addInstanceOrClassVar(DetailAST varDefAst) {
621 final DetailAST parentAst = varDefAst.getParent();
622 final DetailAST grandParentAst = parentAst.getParent();
623 if (grandParentAst != null
624 && isNonLocalTypeDeclaration(grandParentAst)
625 && !isPrivateInstanceVariable(varDefAst)) {
626 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
627 final VariableDesc desc = new VariableDesc(ident.getText());
628 typeDeclAstToTypeDeclDesc.get(grandParentAst).addInstOrClassVar(desc);
629 }
630 }
631
632 /**
633 * Whether instance variable or class variable have private access modifier.
634 *
635 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
636 * @return true if instance variable or class variable have private access modifier
637 */
638 private static boolean isPrivateInstanceVariable(DetailAST varDefAst) {
639 final AccessModifierOption varAccessModifier =
640 CheckUtil.getAccessModifierFromModifiersToken(varDefAst);
641 return varAccessModifier == AccessModifierOption.PRIVATE;
642 }
643
644 /**
645 * Get the {@link TypeDeclDesc} of the super class of anonymous inner class.
646 *
647 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
648 * @return {@link TypeDeclDesc} of the super class of anonymous inner class
649 */
650 private TypeDeclDesc getSuperClassOfAnonInnerClass(DetailAST literalNewAst) {
651 TypeDeclDesc obtainedClass = null;
652 final String shortNameOfClass = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst);
653 if (packageName != null && shortNameOfClass.startsWith(packageName)) {
654 final Optional<TypeDeclDesc> classWithCompletePackageName =
655 typeDeclAstToTypeDeclDesc.values()
656 .stream()
657 .filter(typeDeclDesc -> {
658 return typeDeclDesc.getQualifiedName().equals(shortNameOfClass);
659 })
660 .findFirst();
661 if (classWithCompletePackageName.isPresent()) {
662 obtainedClass = classWithCompletePackageName.orElseThrow();
663 }
664 }
665 else {
666 final List<TypeDeclDesc> typeDeclWithSameName = typeDeclWithSameName(shortNameOfClass);
667 if (!typeDeclWithSameName.isEmpty()) {
668 obtainedClass = getClosestMatchingTypeDeclaration(
669 anonInnerAstToTypeDeclDesc.get(literalNewAst).getQualifiedName(),
670 typeDeclWithSameName);
671 }
672 }
673 return obtainedClass;
674 }
675
676 /**
677 * Add non-private instance and class variables of the super class of the anonymous class
678 * to the variables stack.
679 *
680 * @param obtainedClass super class of the anon inner class
681 * @param variablesStack stack of all the relevant variables in the scope
682 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
683 */
684 private void modifyVariablesStack(TypeDeclDesc obtainedClass,
685 Deque<VariableDesc> variablesStack,
686 DetailAST literalNewAst) {
687 if (obtainedClass != null) {
688 final Deque<VariableDesc> instAndClassVarDeque = typeDeclAstToTypeDeclDesc
689 .get(obtainedClass.getTypeDeclAst())
690 .getUpdatedCopyOfVarStack(literalNewAst);
691 instAndClassVarDeque.forEach(variablesStack::push);
692 }
693 }
694
695 /**
696 * Checks if there is a type declaration with same name as the super class.
697 *
698 * @param superClassName name of the super class
699 * @return list if there is another type declaration with same name.
700 */
701 private List<TypeDeclDesc> typeDeclWithSameName(String superClassName) {
702 return typeDeclAstToTypeDeclDesc.values().stream()
703 .filter(typeDeclDesc -> {
704 return hasSameNameAsSuperClass(superClassName, typeDeclDesc);
705 })
706 .toList();
707 }
708
709 /**
710 * Whether the qualified name of {@code typeDeclDesc} matches the super class name.
711 *
712 * @param superClassName name of the super class
713 * @param typeDeclDesc type declaration description
714 * @return {@code true} if the qualified name of {@code typeDeclDesc}
715 * matches the super class name
716 */
717 private boolean hasSameNameAsSuperClass(String superClassName, TypeDeclDesc typeDeclDesc) {
718 final boolean result;
719 if (packageName == null && typeDeclDesc.getDepth() == 0) {
720 result = typeDeclDesc.getQualifiedName().equals(superClassName);
721 }
722 else {
723 result = typeDeclDesc.getQualifiedName()
724 .endsWith(PACKAGE_SEPARATOR + superClassName);
725 }
726 return result;
727 }
728
729 /**
730 * For all type declarations with the same name as the superclass, gets the nearest type
731 * declaration.
732 *
733 * @param outerTypeDeclName outer type declaration of anonymous inner class
734 * @param typeDeclWithSameName typeDeclarations which have the same name as the super class
735 * @return the nearest class
736 */
737 private static TypeDeclDesc getClosestMatchingTypeDeclaration(String outerTypeDeclName,
738 List<TypeDeclDesc> typeDeclWithSameName) {
739 return Collections.min(typeDeclWithSameName, (first, second) -> {
740 return calculateTypeDeclarationDistance(outerTypeDeclName, first, second);
741 });
742 }
743
744 /**
745 * Get the difference between type declaration name matching count. If the
746 * difference between them is zero, then their depth is compared to obtain the result.
747 *
748 * @param outerTypeName outer type declaration of anonymous inner class
749 * @param firstType first input type declaration
750 * @param secondType second input type declaration
751 * @return difference between type declaration name matching count
752 */
753 private static int calculateTypeDeclarationDistance(String outerTypeName,
754 TypeDeclDesc firstType,
755 TypeDeclDesc secondType) {
756 final int firstMatchCount =
757 countMatchingQualifierChars(outerTypeName, firstType.getQualifiedName());
758 final int secondMatchCount =
759 countMatchingQualifierChars(outerTypeName, secondType.getQualifiedName());
760 final int matchDistance = Integer.compare(secondMatchCount, firstMatchCount);
761
762 final int distance;
763 if (matchDistance == 0) {
764 distance = Integer.compare(firstType.getDepth(), secondType.getDepth());
765 }
766 else {
767 distance = matchDistance;
768 }
769
770 return distance;
771 }
772
773 /**
774 * Calculates the type declaration matching count for the superclass of an anonymous inner
775 * class.
776 *
777 * <p>
778 * For example, if the pattern class is {@code Main.ClassOne} and the class to be matched is
779 * {@code Main.ClassOne.ClassTwo.ClassThree}, then the matching count would be calculated by
780 * comparing the characters at each position, and updating the count whenever a '.'
781 * is encountered.
782 * This is necessary because pattern class can include anonymous inner classes, unlike regular
783 * inheritance where nested classes cannot be extended.
784 * </p>
785 *
786 * @param pattern type declaration to match against
787 * @param candidate type declaration to be matched
788 * @return the type declaration matching count
789 */
790 private static int countMatchingQualifierChars(String pattern,
791 String candidate) {
792 final int typeDeclarationToBeMatchedLength = candidate.length();
793 final int minLength = Math
794 .min(typeDeclarationToBeMatchedLength, pattern.length());
795 final boolean shouldCountBeUpdatedAtLastCharacter =
796 typeDeclarationToBeMatchedLength > minLength
797 && candidate.charAt(minLength) == PACKAGE_SEPARATOR.charAt(0);
798
799 int result = 0;
800 for (int idx = 0;
801 idx < minLength
802 && pattern.charAt(idx) == candidate.charAt(idx);
803 idx++) {
804
805 if (shouldCountBeUpdatedAtLastCharacter
806 || pattern.charAt(idx) == PACKAGE_SEPARATOR.charAt(0)) {
807 result = idx;
808 }
809 }
810 return result;
811 }
812
813 /**
814 * Get qualified type declaration name from type ast.
815 *
816 * @param typeDeclAst type declaration ast
817 * @return qualified name of type declaration
818 */
819 private String getQualifiedTypeDeclarationName(DetailAST typeDeclAst) {
820 final String className = typeDeclAst.findFirstToken(TokenTypes.IDENT).getText();
821 String outerClassQualifiedName = null;
822 if (!typeDeclarations.isEmpty()) {
823 outerClassQualifiedName = typeDeclarations.peek().getQualifiedName();
824 }
825 return CheckUtil
826 .getQualifiedTypeDeclarationName(packageName, outerClassQualifiedName, className);
827 }
828
829 /**
830 * Iterate over all the ast nodes present under {@code ast}.
831 *
832 * @param ast ast
833 * @param variablesStack stack of all the relevant variables in the scope
834 */
835 private void iterateOverBlockContainingLocalAnonInnerClass(
836 DetailAST ast, Deque<VariableDesc> variablesStack) {
837 DetailAST currNode = ast;
838 while (currNode != null) {
839 customVisitToken(currNode, variablesStack);
840 DetailAST toVisit = currNode.getFirstChild();
841 while (currNode != ast && toVisit == null) {
842 customLeaveToken(currNode, variablesStack);
843 toVisit = currNode.getNextSibling();
844 currNode = currNode.getParent();
845 }
846 currNode = toVisit;
847 }
848 }
849
850 /**
851 * Visit all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once
852 * again.
853 *
854 * @param ast ast
855 * @param variablesStack stack of all the relevant variables in the scope
856 */
857 private void customVisitToken(DetailAST ast, Deque<VariableDesc> variablesStack) {
858 final int type = ast.getType();
859 switch (type) {
860 case TokenTypes.DOT -> visitDotToken(ast, variablesStack);
861
862 case TokenTypes.VARIABLE_DEF -> addLocalVariables(ast, variablesStack);
863
864 case TokenTypes.IDENT -> visitIdentToken(ast, variablesStack);
865
866 case TokenTypes.LITERAL_NEW -> {
867 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
868 final TypeDeclDesc obtainedClass = getSuperClassOfAnonInnerClass(ast);
869 modifyVariablesStack(obtainedClass, variablesStack, ast);
870 }
871 }
872
873 default -> {
874 // No action needed for other token types
875 }
876 }
877 }
878
879 /**
880 * Leave all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once
881 * again.
882 *
883 * @param ast ast
884 * @param variablesStack stack of all the relevant variables in the scope
885 */
886 private void customLeaveToken(DetailAST ast, Deque<VariableDesc> variablesStack) {
887 logViolations(ast, variablesStack);
888 }
889
890 /**
891 * Whether to check identifier token nested under dotAst.
892 *
893 * @param dotAst dotAst
894 * @return true if ident nested under dotAst should be checked
895 */
896 private static boolean shouldCheckIdentTokenNestedUnderDot(DetailAST dotAst) {
897
898 return TokenUtil.findFirstTokenByPredicate(dotAst,
899 childAst -> {
900 return TokenUtil.isOfType(childAst,
901 UNACCEPTABLE_CHILD_OF_DOT);
902 })
903 .isEmpty();
904 }
905
906 /**
907 * Checks the identifier ast.
908 *
909 * @param identAst ast of type {@link TokenTypes#IDENT}
910 * @param variablesStack stack of all the relevant variables in the scope
911 */
912 private static void checkIdentifierAst(DetailAST identAst, Deque<VariableDesc> variablesStack) {
913 for (VariableDesc variableDesc : variablesStack) {
914 if (identAst.getText().equals(variableDesc.getName())
915 && !isLeftHandSideValue(identAst)) {
916 variableDesc.registerAsUsed();
917 break;
918 }
919 }
920 }
921
922 /**
923 * Find the scope of variable.
924 *
925 * @param variableDef ast of type {@link TokenTypes#VARIABLE_DEF}
926 * @return scope of variableDef
927 */
928 private static DetailAST findScopeOfVariable(DetailAST variableDef) {
929 final DetailAST result;
930 final DetailAST parentAst = variableDef.getParent();
931 if (TokenUtil.isOfType(parentAst, TokenTypes.SLIST, TokenTypes.OBJBLOCK)) {
932 result = parentAst;
933 }
934 else {
935 result = parentAst.getParent();
936 }
937 return result;
938 }
939
940 /**
941 * Checks whether the ast of type {@link TokenTypes#IDENT} is
942 * used as left-hand side value. An identifier is being used as a left-hand side
943 * value if it is used as the left operand of an assignment or as an
944 * operand of a stand-alone increment or decrement.
945 *
946 * @param identAst ast of type {@link TokenTypes#IDENT}
947 * @return true if identAst is used as a left-hand side value
948 */
949 private static boolean isLeftHandSideValue(DetailAST identAst) {
950 final DetailAST parent = identAst.getParent();
951 return isStandAloneIncrementOrDecrement(identAst)
952 || parent.getType() == TokenTypes.ASSIGN
953 && identAst != parent.getLastChild();
954 }
955
956 /**
957 * Checks whether the ast of type {@link TokenTypes#IDENT} is used as
958 * an operand of a stand-alone increment or decrement.
959 *
960 * @param identAst ast of type {@link TokenTypes#IDENT}
961 * @return true if identAst is used as an operand of stand-alone
962 * increment or decrement
963 */
964 private static boolean isStandAloneIncrementOrDecrement(DetailAST identAst) {
965 final DetailAST parent = identAst.getParent();
966 final DetailAST grandParent = parent.getParent();
967 return TokenUtil.isOfType(parent, INCREMENT_AND_DECREMENT_TOKENS)
968 && TokenUtil.isOfType(grandParent, TokenTypes.EXPR)
969 && !isIncrementOrDecrementVariableUsed(grandParent);
970 }
971
972 /**
973 * A variable with increment or decrement operator is considered used if it
974 * is used as an argument or as an array index or for assigning value
975 * to a variable.
976 *
977 * @param exprAst ast of type {@link TokenTypes#EXPR}
978 * @return true if variable nested in exprAst is used
979 */
980 private static boolean isIncrementOrDecrementVariableUsed(DetailAST exprAst) {
981 return TokenUtil.isOfType(exprAst.getParent(), INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES)
982 && exprAst.getParent().getParent().getType() != TokenTypes.FOR_ITERATOR;
983 }
984
985 /**
986 * Maintains information about the variable.
987 */
988 private static final class VariableDesc {
989
990 /**
991 * The name of the variable.
992 */
993 private final String name;
994
995 /**
996 * Ast of type {@link TokenTypes#TYPE}.
997 */
998 private final DetailAST typeAst;
999
1000 /**
1001 * The scope of variable is determined by the ast of type
1002 * {@link TokenTypes#SLIST} or {@link TokenTypes#LITERAL_FOR}
1003 * or {@link TokenTypes#OBJBLOCK} which is enclosing the variable.
1004 */
1005 private final DetailAST scope;
1006
1007 /**
1008 * Is an instance variable or a class variable.
1009 */
1010 private boolean instVarOrClassVar;
1011
1012 /**
1013 * Is a named pattern variable declared in a switch label.
1014 */
1015 private boolean namedPatternVar;
1016
1017 /**
1018 * Is the variable used.
1019 */
1020 private boolean used;
1021
1022 /**
1023 * Create a new VariableDesc instance.
1024 *
1025 * @param name name of the variable
1026 */
1027 private VariableDesc(String name) {
1028 this(name, null, null);
1029 }
1030
1031 /**
1032 * Create a new VariableDesc instance.
1033 *
1034 * @param name name of the variable
1035 * @param scope ast of type {@link TokenTypes#SLIST} or
1036 * {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
1037 * which is enclosing the variable
1038 */
1039 private VariableDesc(String name, DetailAST scope) {
1040 this(name, null, scope);
1041 }
1042
1043 /**
1044 * Create a new VariableDesc instance.
1045 *
1046 * @param name name of the variable
1047 * @param typeAst ast of type {@link TokenTypes#TYPE}
1048 * @param scope ast of type {@link TokenTypes#SLIST} or
1049 * {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
1050 * which is enclosing the variable
1051 */
1052 private VariableDesc(String name, DetailAST typeAst, DetailAST scope) {
1053 this.name = name;
1054 this.typeAst = typeAst;
1055 this.scope = scope;
1056 }
1057
1058 /**
1059 * Get the name of variable.
1060 *
1061 * @return name of variable
1062 */
1063 /* package */ String getName() {
1064 return name;
1065 }
1066
1067 /**
1068 * Get the associated ast node of type {@link TokenTypes#TYPE}.
1069 *
1070 * @return the associated ast node of type {@link TokenTypes#TYPE}
1071 */
1072 /* package */ DetailAST getTypeAst() {
1073 return typeAst;
1074 }
1075
1076 /**
1077 * Get ast of type {@link TokenTypes#SLIST}
1078 * or {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
1079 * which is enclosing the variable i.e. its scope.
1080 *
1081 * @return the scope associated with the variable
1082 */
1083 /* package */ DetailAST getScope() {
1084 return scope;
1085 }
1086
1087 /**
1088 * Register the variable as used.
1089 */
1090 /* package */ void registerAsUsed() {
1091 used = true;
1092 }
1093
1094 /**
1095 * Register the variable as an instance variable or
1096 * class variable.
1097 */
1098 /* package */ void registerAsInstOrClassVar() {
1099 instVarOrClassVar = true;
1100 }
1101
1102 /**
1103 * Register the variable as a forced-name pattern variable declared
1104 * in a switch label or instanceof record Destructuring.
1105 */
1106 /* package */ void registerAsNamedPatternVar() {
1107 namedPatternVar = true;
1108 }
1109
1110 /**
1111 * Is the variable used or not.
1112 *
1113 * @return true if variable is used
1114 */
1115 /* package */ boolean isUsed() {
1116 return used;
1117 }
1118
1119 /**
1120 * Is an instance variable or a class variable.
1121 *
1122 * @return true if is an instance variable or a class variable
1123 */
1124 /* package */ boolean isInstVarOrClassVar() {
1125 return instVarOrClassVar;
1126 }
1127
1128 /**
1129 * Is a forced-name pattern variable from a switch label or
1130 * instanceof record Destructuring.
1131 *
1132 * @return true if this variable was declared in a context where
1133 * pre-JDK 22 forces a name to be given even when unused
1134 */
1135 /* package */ boolean isNamedPatternVar() {
1136 return namedPatternVar;
1137 }
1138 }
1139
1140 /**
1141 * Maintains information about the type declaration.
1142 * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF}
1143 * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF}
1144 * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration.
1145 */
1146 private static final class TypeDeclDesc {
1147
1148 /**
1149 * Complete type declaration name with package name and outer type declaration name.
1150 */
1151 private final String qualifiedName;
1152
1153 /**
1154 * Depth of nesting of type declaration.
1155 */
1156 private final int depth;
1157
1158 /**
1159 * Type declaration ast node.
1160 */
1161 private final DetailAST typeDeclAst;
1162
1163 /**
1164 * A stack of type declaration's instance and static variables.
1165 */
1166 private final Deque<VariableDesc> instanceAndClassVarStack;
1167
1168 /**
1169 * Create a new TypeDeclDesc instance.
1170 *
1171 * @param qualifiedName qualified name
1172 * @param depth depth of nesting
1173 * @param typeDeclAst type declaration ast node
1174 */
1175 private TypeDeclDesc(String qualifiedName, int depth,
1176 DetailAST typeDeclAst) {
1177 this.qualifiedName = qualifiedName;
1178 this.depth = depth;
1179 this.typeDeclAst = typeDeclAst;
1180 instanceAndClassVarStack = new ArrayDeque<>();
1181 }
1182
1183 /**
1184 * Get the complete type declaration name i.e. type declaration name with package name
1185 * and outer type declaration name.
1186 *
1187 * @return qualified class name
1188 */
1189 /* package */ String getQualifiedName() {
1190 return qualifiedName;
1191 }
1192
1193 /**
1194 * Get the depth of type declaration.
1195 *
1196 * @return the depth of nesting of type declaration
1197 */
1198 /* package */ int getDepth() {
1199 return depth;
1200 }
1201
1202 /**
1203 * Get the type declaration ast node.
1204 *
1205 * @return ast node of the type declaration
1206 */
1207 /* package */ DetailAST getTypeDeclAst() {
1208 return typeDeclAst;
1209 }
1210
1211 /**
1212 * Get the copy of variables in instanceAndClassVar stack with updated scope.
1213 *
1214 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
1215 * @return copy of variables in instanceAndClassVar stack with updated scope.
1216 */
1217 /* package */ Deque<VariableDesc> getUpdatedCopyOfVarStack(DetailAST literalNewAst) {
1218 final DetailAST updatedScope = literalNewAst;
1219 final Deque<VariableDesc> instAndClassVarDeque = new ArrayDeque<>();
1220 instanceAndClassVarStack.forEach(instVar -> {
1221 final VariableDesc variableDesc = new VariableDesc(instVar.getName(),
1222 updatedScope);
1223 variableDesc.registerAsInstOrClassVar();
1224 instAndClassVarDeque.push(variableDesc);
1225 });
1226 return instAndClassVarDeque;
1227 }
1228
1229 /**
1230 * Add an instance variable or class variable to the stack.
1231 *
1232 * @param variableDesc variable to be added
1233 */
1234 /* package */ void addInstOrClassVar(VariableDesc variableDesc) {
1235 instanceAndClassVarStack.push(variableDesc);
1236 }
1237 }
1238 }