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 }; 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 {@link 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}