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.Deque;
24
25 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
26 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27 import com.puppycrawl.tools.checkstyle.api.DetailAST;
28 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
30
31 /**
32 * <div>
33 * Ensures that try-with-resources resource variables that are not used
34 * are declared as an unnamed variable.
35 * </div>
36 *
37 * <p>
38 * Rationale:
39 * </p>
40 * <ul>
41 * <li>
42 * Improves code readability by clearly indicating which resources are unused.
43 * </li>
44 * <li>
45 * Follows Java conventions for denoting unused variables with an underscore
46 * ({@code _}).
47 * </li>
48 * </ul>
49 *
50 * <p>
51 * Only declared resources inside the try-with-resources parentheses are checked
52 * (i.e. {@code var a = lock()} or {@code AutoCloseable a = lock()}).
53 * Resources that are referenced but not declared inside the try
54 * (e.g. {@code try (releaser) { }}) are never flagged, because those resources
55 * cannot be replaced with {@code _}.
56 * </p>
57 *
58 * <p>
59 * See the <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
60 * Java Language Specification</a> for more information about unnamed variables.
61 * </p>
62 *
63 * <p>
64 * <b>Attention</b>: This check should be activated only on source code
65 * that is compiled by jdk21 or higher;
66 * unnamed variables came out as a preview feature in Java 21 and
67 * became a standard part of the language in Java 22.
68 * </p>
69 *
70 * @since 13.5.0
71 */
72 @FileStatefulCheck
73 public class UnusedTryResourceShouldBeUnnamedCheck extends AbstractCheck {
74
75 /**
76 * A key pointing to the warning message text in "messages.properties" file.
77 */
78 public static final String MSG_UNUSED_TRY_RESOURCE = "unused.try.resource";
79
80 /**
81 * The unnamed variable identifier introduced in Java 21.
82 */
83 private static final String UNNAMED_VARIABLE_IDENTIFIER = "_";
84
85 /**
86 * Parent token types for an {@link TokenTypes#IDENT} that indicate the identifier
87 * is <em>not</em> a plain variable reference and should therefore be excluded from
88 * "used" detection.
89 */
90 private static final int[] INVALID_RESOURCE_IDENT_PARENTS = {
91 TokenTypes.DOT,
92 TokenTypes.LITERAL_NEW,
93 TokenTypes.METHOD_CALL,
94 TokenTypes.TYPE,
95 };
96
97 /**
98 * A stack of per-try resource-detail lists.
99 */
100 private final Deque<Deque<TryResourceDetails>> tryResources = new ArrayDeque<>();
101
102 /**
103 * Creates a new {@code UnusedTryResourceShouldBeUnnamedCheck} instance.
104 */
105 public UnusedTryResourceShouldBeUnnamedCheck() {
106 // no code by default
107 }
108
109 @Override
110 public int[] getDefaultTokens() {
111 return getRequiredTokens();
112 }
113
114 @Override
115 public int[] getAcceptableTokens() {
116 return getRequiredTokens();
117 }
118
119 @Override
120 public int[] getRequiredTokens() {
121 return new int[] {
122 TokenTypes.LITERAL_TRY,
123 TokenTypes.IDENT,
124 TokenTypes.SLIST,
125 };
126 }
127
128 @Override
129 public void beginTree(DetailAST rootAST) {
130 tryResources.clear();
131 }
132
133 @Override
134 public void visitToken(DetailAST ast) {
135 switch (ast.getType()) {
136 case TokenTypes.LITERAL_TRY -> tryResources.push(collectTrackedResources(ast));
137 case TokenTypes.IDENT -> {
138 if (isResourceUsageCandidate(ast)) {
139 tryResources.stream()
140 .flatMap(Deque::stream)
141 .filter(resource -> resource.getName().equals(ast.getText()))
142 .findFirst()
143 .ifPresent(TryResourceDetails::registerAsUsed);
144 }
145 }
146 default -> {
147 // SLIST is needed only when leaving the try body.
148 }
149 }
150 }
151
152 @Override
153 public void leaveToken(DetailAST ast) {
154 if (ast.getParent().getType() == TokenTypes.LITERAL_TRY) {
155 final Deque<TryResourceDetails> resources = tryResources.peek();
156 for (TryResourceDetails resource : resources) {
157 if (!resource.isUsed()) {
158 log(resource.getIdentToken(),
159 MSG_UNUSED_TRY_RESOURCE,
160 resource.getName());
161 }
162 }
163 tryResources.pop();
164 }
165 }
166
167 /**
168 * Collects all tracked resources from the {@code RESOURCE_SPECIFICATION} of a
169 * try-with-resources statement.
170 *
171 * @param tryAst the {@link TokenTypes#LITERAL_TRY} token
172 * @return a deque of {@link TryResourceDetails} for trackable resources;
173 * never {@code null}, but may be empty for plain try statements
174 */
175 private static Deque<TryResourceDetails> collectTrackedResources(DetailAST tryAst) {
176 final Deque<TryResourceDetails> resources = new ArrayDeque<>();
177 final DetailAST resourceSpec =
178 tryAst.findFirstToken(TokenTypes.RESOURCE_SPECIFICATION);
179 if (resourceSpec != null) {
180 final DetailAST resourcesNode =
181 resourceSpec.findFirstToken(TokenTypes.RESOURCES);
182
183 TokenUtil.forEachChild(resourcesNode, TokenTypes.RESOURCE, child -> {
184 final boolean isDeclared = child.findFirstToken(TokenTypes.TYPE) != null;
185 if (isDeclared) {
186 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
187 if (!UNNAMED_VARIABLE_IDENTIFIER.equals(ident.getText())) {
188 resources.addLast(new TryResourceDetails(ident));
189 }
190 }
191 });
192 }
193 return resources;
194 }
195
196 /**
197 * Determines whether an {@link TokenTypes#IDENT} token is a candidate for being
198 * a <em>use</em> of a tracked try resource.
199 *
200 * @param identAst the {@code TokenTypes#IDENT} token to inspect
201 * @return {@code true} if the token could represent a reference to a resource variable
202 */
203 private static boolean isResourceUsageCandidate(DetailAST identAst) {
204 return !isResourceDeclarationIdent(identAst)
205 && (!TokenUtil.isOfType(identAst.getParent(), INVALID_RESOURCE_IDENT_PARENTS)
206 || isObjectReferenceInDot(identAst));
207 }
208
209 /**
210 * Returns {@code true} when {@code identAst} is the variable-name token inside a
211 * {@link TokenTypes#RESOURCE} node (i.e. the declaration site, not a use).
212 *
213 * @param identAst the {@link TokenTypes#IDENT} token
214 * @return {@code true} if this IDENT is the name in a resource declaration/reference
215 */
216 private static boolean isResourceDeclarationIdent(DetailAST identAst) {
217 final DetailAST parent = identAst.getParent();
218 return parent.getType() == TokenTypes.RESOURCE
219 && parent.findFirstToken(TokenTypes.TYPE) != null;
220 }
221
222 /**
223 * Returns {@code true} when {@code identAst} is the <em>first</em> child of a
224 * {@link TokenTypes#DOT} node, meaning it is the object reference in an expression
225 * such as {@code a.close()} — a genuine use of the variable.
226 *
227 * @param identAst the {@link TokenTypes#IDENT} token
228 * @return {@code true} if the IDENT is the left-hand operand of a dot expression
229 */
230 private static boolean isObjectReferenceInDot(DetailAST identAst) {
231 final DetailAST parent = identAst.getParent();
232 return parent.getType() == TokenTypes.DOT
233 && identAst.equals(parent.getFirstChild());
234 }
235
236 /**
237 * Maintains tracking information about a single try-with-resources resource.
238 */
239 private static final class TryResourceDetails {
240
241 /** The name of the resource variable. */
242 private final String name;
243
244 /**
245 * The {@link TokenTypes#IDENT} token for the variable name.
246 * Used as the violation position.
247 */
248 private final DetailAST identToken;
249
250 /** Whether the resource has been referenced within the try scope. */
251 private boolean used;
252
253 /**
254 * Creates a new instance tracking the resource whose name-token is
255 * {@code identToken}.
256 *
257 * @param identToken the {@link TokenTypes#IDENT} token for the resource name
258 */
259 private TryResourceDetails(DetailAST identToken) {
260 name = identToken.getText();
261 this.identToken = identToken;
262 }
263
264 /**
265 * Marks this resource as having been referenced (used) in the try scope.
266 */
267 private void registerAsUsed() {
268 used = true;
269 }
270
271 /**
272 * Returns the name of the resource variable.
273 *
274 * @return variable name
275 */
276 private String getName() {
277 return name;
278 }
279
280 /**
281 * Returns the {@link TokenTypes#IDENT} token used to report violations.
282 *
283 * @return IDENT token
284 */
285 private DetailAST getIdentToken() {
286 return identToken;
287 }
288
289 /**
290 * Returns whether this resource has been referenced in the try scope.
291 *
292 * @return {@code true} if used
293 */
294 private boolean isUsed() {
295 return used;
296 }
297 }
298
299 }