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}