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 @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}