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     /**
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 }