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}