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 };
125 }
126
127 @Override
128 public void beginTree(DetailAST rootAST) {
129 tryResources.clear();
130 }
131
132 @Override
133 public void visitToken(DetailAST ast) {
134 if (ast.getType() == TokenTypes.LITERAL_TRY) {
135 tryResources.push(collectTrackedResources(ast));
136 }
137 else if (isResourceUsageCandidate(ast)
138 && !isShadowedByCatchParameter(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
147 @Override
148 public void leaveToken(DetailAST ast) {
149 if (ast.getType() == TokenTypes.LITERAL_TRY) {
150 final Deque<TryResourceDetails> resources = tryResources.peek();
151 for (TryResourceDetails resource : resources) {
152 if (!resource.isUsed()) {
153 log(resource.getIdentToken(),
154 MSG_UNUSED_TRY_RESOURCE,
155 resource.getName());
156 }
157 }
158 tryResources.pop();
159 }
160 }
161
162 /**
163 * Collects all tracked resources from the {@code RESOURCE_SPECIFICATION} of a
164 * try-with-resources statement.
165 *
166 * @param tryAst the {@link TokenTypes#LITERAL_TRY} token
167 * @return a deque of {@link TryResourceDetails} for trackable resources;
168 * never {@code null}, but may be empty for plain try statements
169 */
170 private static Deque<TryResourceDetails> collectTrackedResources(DetailAST tryAst) {
171 final Deque<TryResourceDetails> resources = new ArrayDeque<>();
172 final DetailAST resourceSpec =
173 tryAst.findFirstToken(TokenTypes.RESOURCE_SPECIFICATION);
174 if (resourceSpec != null) {
175 final DetailAST resourcesNode =
176 resourceSpec.findFirstToken(TokenTypes.RESOURCES);
177
178 TokenUtil.forEachChild(resourcesNode, TokenTypes.RESOURCE, child -> {
179 final boolean isDeclared = child.findFirstToken(TokenTypes.TYPE) != null;
180 if (isDeclared) {
181 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
182 if (!UNNAMED_VARIABLE_IDENTIFIER.equals(ident.getText())) {
183 resources.addLast(new TryResourceDetails(ident));
184 }
185 }
186 });
187 }
188 return resources;
189 }
190
191 /**
192 * Determines whether an {@link TokenTypes#IDENT} token is a candidate for being
193 * a <em>use</em> of a tracked try resource.
194 *
195 * @param identAst the {@code TokenTypes#IDENT} token to inspect
196 * @return {@code true} if the token could represent a reference to a resource variable
197 */
198 private static boolean isResourceUsageCandidate(DetailAST identAst) {
199 return !isResourceDeclarationIdent(identAst)
200 && (!TokenUtil.isOfType(identAst.getParent(), INVALID_RESOURCE_IDENT_PARENTS)
201 || isObjectReferenceInDot(identAst));
202 }
203
204 /**
205 * Returns {@code true} when {@code identAst} is shadowed by a catch parameter
206 * of an immediately enclosing {@link TokenTypes#LITERAL_CATCH} block.
207 *
208 * @param identAst the {@link TokenTypes#IDENT} token to inspect
209 * @return {@code true} if a catch parameter with the same name is in scope
210 */
211 private static boolean isShadowedByCatchParameter(DetailAST identAst) {
212 boolean shadowed = false;
213 DetailAST ancestor = identAst;
214 while (ancestor != null) {
215 if (ancestor.getType() == TokenTypes.LITERAL_CATCH) {
216 final DetailAST paramDef =
217 ancestor.findFirstToken(TokenTypes.PARAMETER_DEF);
218 final DetailAST paramIdent =
219 paramDef.findFirstToken(TokenTypes.IDENT);
220 shadowed = paramIdent.getText().equals(identAst.getText());
221 break;
222 }
223 ancestor = ancestor.getParent();
224 }
225 return shadowed;
226 }
227
228 /**
229 * Returns {@code true} when {@code identAst} is the variable-name token inside a
230 * {@link TokenTypes#RESOURCE} node (i.e. the declaration site, not a use).
231 *
232 * @param identAst the {@link TokenTypes#IDENT} token
233 * @return {@code true} if this IDENT is the name in a resource declaration/reference
234 */
235 private static boolean isResourceDeclarationIdent(DetailAST identAst) {
236 final DetailAST parent = identAst.getParent();
237 return parent.getType() == TokenTypes.RESOURCE
238 && parent.findFirstToken(TokenTypes.TYPE) != null;
239 }
240
241 /**
242 * Returns {@code true} when {@code identAst} is the <em>first</em> child of a
243 * {@link TokenTypes#DOT} node, meaning it is the object reference in an expression
244 * such as {@code a.close()} — a genuine use of the variable.
245 *
246 * @param identAst the {@link TokenTypes#IDENT} token
247 * @return {@code true} if the IDENT is the left-hand operand of a dot expression
248 */
249 private static boolean isObjectReferenceInDot(DetailAST identAst) {
250 final DetailAST parent = identAst.getParent();
251 return parent.getType() == TokenTypes.DOT
252 && identAst.equals(parent.getFirstChild());
253 }
254
255 /**
256 * Maintains tracking information about a single try-with-resources resource.
257 */
258 private static final class TryResourceDetails {
259
260 /** The name of the resource variable. */
261 private final String name;
262
263 /**
264 * The {@link TokenTypes#IDENT} token for the variable name.
265 * Used as the violation position.
266 */
267 private final DetailAST identToken;
268
269 /** Whether the resource has been referenced within the try scope. */
270 private boolean used;
271
272 /**
273 * Creates a new instance tracking the resource whose name-token is
274 * {@code identToken}.
275 *
276 * @param identToken the {@link TokenTypes#IDENT} token for the resource name
277 */
278 private TryResourceDetails(DetailAST identToken) {
279 name = identToken.getText();
280 this.identToken = identToken;
281 }
282
283 /**
284 * Marks this resource as having been referenced (used) in the try scope.
285 */
286 private void registerAsUsed() {
287 used = true;
288 }
289
290 /**
291 * Returns the name of the resource variable.
292 *
293 * @return variable name
294 */
295 private String getName() {
296 return name;
297 }
298
299 /**
300 * Returns the {@link TokenTypes#IDENT} token used to report violations.
301 *
302 * @return IDENT token
303 */
304 private DetailAST getIdentToken() {
305 return identToken;
306 }
307
308 /**
309 * Returns whether this resource has been referenced in the try scope.
310 *
311 * @return {@code true} if used
312 */
313 private boolean isUsed() {
314 return used;
315 }
316 }
317
318 }