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