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