001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.coding; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 030 031/** 032 * <div> 033 * Ensures that try-with-resources resource variables that are not used 034 * are declared as an unnamed variable. 035 * </div> 036 * 037 * <p> 038 * Rationale: 039 * </p> 040 * <ul> 041 * <li> 042 * Improves code readability by clearly indicating which resources are unused. 043 * </li> 044 * <li> 045 * Follows Java conventions for denoting unused variables with an underscore 046 * ({@code _}). 047 * </li> 048 * </ul> 049 * 050 * <p> 051 * Only declared resources inside the try-with-resources parentheses are checked 052 * (i.e. {@code var a = lock()} or {@code AutoCloseable a = lock()}). 053 * Resources that are referenced but not declared inside the try 054 * (e.g. {@code try (releaser) { }}) are never flagged, because those resources 055 * cannot be replaced with {@code _}. 056 * </p> 057 * 058 * <p> 059 * See the <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html"> 060 * Java Language Specification</a> for more information about unnamed variables. 061 * </p> 062 * 063 * <p> 064 * <b>Attention</b>: This check should be activated only on source code 065 * that is compiled by jdk21 or higher; 066 * unnamed variables came out as a preview feature in Java 21 and 067 * became a standard part of the language in Java 22. 068 * </p> 069 * 070 * @since 13.5.0 071 */ 072@FileStatefulCheck 073public class UnusedTryResourceShouldBeUnnamedCheck extends AbstractCheck { 074 075 /** 076 * A key pointing to the warning message text in "messages.properties" file. 077 */ 078 public static final String MSG_UNUSED_TRY_RESOURCE = "unused.try.resource"; 079 080 /** 081 * The unnamed variable identifier introduced in Java 21. 082 */ 083 private static final String UNNAMED_VARIABLE_IDENTIFIER = "_"; 084 085 /** 086 * Parent token types for an {@link TokenTypes#IDENT} that indicate the identifier 087 * is <em>not</em> a plain variable reference and should therefore be excluded from 088 * "used" detection. 089 */ 090 private static final int[] INVALID_RESOURCE_IDENT_PARENTS = { 091 TokenTypes.DOT, 092 TokenTypes.LITERAL_NEW, 093 TokenTypes.METHOD_CALL, 094 TokenTypes.TYPE, 095 }; 096 097 /** 098 * A stack of per-try resource-detail lists. 099 */ 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}