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}