View Javadoc
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 }