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.ArrayList;
023import java.util.Collection;
024import java.util.List;
025import java.util.Set;
026
027import javax.annotation.Nullable;
028
029import com.puppycrawl.tools.checkstyle.StatelessCheck;
030import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033
034/**
035 * <div>
036 * Checks that pattern variables that never have their values changed are declared final.
037 * </div>
038 *
039 * <p>
040 * Pattern variables are not implicitly final in Java, and they can be reassigned.
041 * This check verifies that pattern variables are explicitly declared as final if they
042 * are not reassigned within their flow scope.
043 * </p>
044 *
045 * @since 14.4.0
046 */
047@StatelessCheck
048public class FinalPatternVariableCheck extends AbstractCheck {
049
050    /**
051     * A key is pointing to the warning message in "messages.properties" file.
052     */
053    public static final String MSG_KEY = "final.pattern.variable";
054
055    /**
056     * The set of all valid types of ASSIGN token for this check.
057     */
058    private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
059        TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN,
060        TokenTypes.STAR_ASSIGN, TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN,
061        TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN, TokenTypes.SL_ASSIGN,
062        TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN, TokenTypes.BOR_ASSIGN);
063
064    /**
065     * Creates a new {@code FinalPatternVariableCheck} instance.
066     */
067    public FinalPatternVariableCheck() {
068        // no code by default
069    }
070
071    @Override
072    public int[] getRequiredTokens() {
073        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
074    }
075
076    @Override
077    public int[] getDefaultTokens() {
078        return getRequiredTokens();
079    }
080
081    @Override
082    public int[] getAcceptableTokens() {
083        return getRequiredTokens();
084    }
085
086    @Override
087    public void visitToken(final DetailAST ast) {
088
089        final List<DetailAST> patternVariableIdents =
090                getPatternVariableIdents(ast);
091        final List<DetailAST> reassignedVariableIdents =
092                getReassignedVariableIdents(ast);
093
094        for (DetailAST patternVariableIdent : patternVariableIdents) {
095            if (shouldBeFinal(patternVariableIdent,
096                    reassignedVariableIdents)) {
097                log(patternVariableIdent, MSG_KEY,
098                        patternVariableIdent.getText());
099            }
100        }
101    }
102
103    /**
104     * Gets the list of all pattern variable idents in instanceof expression.
105     *
106     * @param ast ast tree of instanceof to get the list from.
107     * @return list of pattern variables.
108     */
109    private static List<DetailAST> getPatternVariableIdents(
110            final DetailAST ast) {
111
112        final DetailAST outermostPatternVariable =
113            ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);
114
115        final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();
116
117        if (outermostPatternVariable != null) {
118            final DetailAST ident =
119                outermostPatternVariable.findFirstToken(TokenTypes.IDENT);
120            patternVariableIdentsArray.add(ident);
121        }
122        else {
123            final DetailAST recordPatternDef;
124            if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
125                recordPatternDef =
126                    ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
127            }
128            else {
129                recordPatternDef = ast;
130            }
131            if (recordPatternDef != null) {
132                collectRecordPatternIdents(recordPatternDef, patternVariableIdentsArray);
133            }
134        }
135        return patternVariableIdentsArray;
136    }
137
138    /**
139     * Collects pattern variable identifiers from a record pattern AST definition.
140     *
141     * @param recordPatternDef AST of record pattern definition
142     * @param patternVariableIdentsArray collection to add pattern variable identifiers to
143     */
144    private static void collectRecordPatternIdents(
145            final DetailAST recordPatternDef,
146            final Collection<DetailAST> patternVariableIdentsArray) {
147        final DetailAST recordPatternComponents = recordPatternDef
148            .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);
149
150        if (recordPatternComponents != null) {
151            for (DetailAST innerPatternVariable =
152                     recordPatternComponents.getFirstChild();
153                 innerPatternVariable != null;
154                 innerPatternVariable =
155                     innerPatternVariable.getNextSibling()) {
156
157                if (innerPatternVariable.getType()
158                        == TokenTypes.PATTERN_VARIABLE_DEF) {
159                    final DetailAST ident =
160                        innerPatternVariable.findFirstToken(
161                            TokenTypes.IDENT);
162                    patternVariableIdentsArray.add(ident);
163                }
164                else {
165                    patternVariableIdentsArray.addAll(
166                        getPatternVariableIdents(innerPatternVariable));
167                }
168            }
169        }
170    }
171
172    /**
173     * Gets the list of AST branches of reassigned variable identifiers.
174     *
175     * @param ast ast tree of checked instanceof statement
176     * @return list of AST identifiers that represent reassigned variables
177     */
178    private static List<DetailAST> getReassignedVariableIdents(
179            final DetailAST ast) {
180
181        final List<DetailAST> reassignedVariableIdents = new ArrayList<>();
182        final DetailAST scopeRoot = findReassignmentScopeRoot(ast);
183
184        if (scopeRoot != null) {
185
186            final List<DetailAST> branches =
187                    expandReassignmentScopes(scopeRoot);
188
189            for (DetailAST branch : branches) {
190                for (DetailAST expressionBranch = branch;
191                     expressionBranch != null;
192                     expressionBranch = shiftToNextTraversedBranch(
193                             expressionBranch, branch)) {
194
195                    final DetailAST assignToken =
196                            getMatchedAssignToken(expressionBranch);
197
198                    if (assignToken != null) {
199                        reassignedVariableIdents.add(
200                                assignToken.getFirstChild());
201                    }
202                }
203            }
204        }
205
206        return reassignedVariableIdents;
207    }
208
209    /**
210     * Gets statements that follow conditional where pattern variable scope
211     * extends. Only returns top-level statements that are siblings of the
212     * conditional, excluding statements nested in control structures like while loops.
213     *
214     * @param conditionalStatement The if statement.
215     * @return List of statement nodes in the extended scope.
216     */
217    private static List<DetailAST> getStatementsInExtendedScope(
218            final DetailAST conditionalStatement) {
219        final List<DetailAST> statements = new ArrayList<>();
220
221        DetailAST nextSibling = conditionalStatement.getNextSibling();
222
223        while (nextSibling != null) {
224            final int type = nextSibling.getType();
225            if (type == TokenTypes.EXPR || type == TokenTypes.LITERAL_RETURN
226                    || type == TokenTypes.LITERAL_IF) {
227                statements.add(nextSibling);
228            }
229            else {
230                if (type != TokenTypes.EMPTY_STAT) {
231                    break;
232                }
233            }
234            nextSibling = nextSibling.getNextSibling();
235        }
236
237        return statements;
238    }
239
240    /**
241     * Shifts once to the next possible branch within traverse trajectory.
242     *
243     * @param ast AST branch to shift from.
244     * @param boundAst AST Branch that traverse cannot extend to.
245     * @return next possible branch within traverse trajectory.
246     */
247    @Nullable
248    private static DetailAST shiftToNextTraversedBranch(
249            final DetailAST ast, final DetailAST boundAst) {
250        DetailAST newAst = ast;
251
252        if (ast.getFirstChild() != null) {
253            newAst = ast.getFirstChild();
254        }
255        else {
256            while (newAst.getNextSibling() == null
257                    && !newAst.equals(boundAst)) {
258                newAst = newAst.getParent();
259            }
260            if (newAst.equals(boundAst)) {
261                newAst = null;
262            }
263            else {
264                newAst = newAst.getNextSibling();
265            }
266        }
267
268        return newAst;
269    }
270
271    /**
272     * Gets the type of ASSIGN tokens that matches with what follows the preceding branch.
273     *
274     * @param preAssignBranch branch preceding ASSIGN token types.
275     * @return type of ASSIGN token.
276     */
277    @Nullable
278    private static DetailAST getMatchedAssignToken(
279            final DetailAST preAssignBranch) {
280        DetailAST matchedAssignToken = null;
281
282        for (int assignType : ASSIGN_TOKEN_TYPES) {
283            final DetailAST assignToken =
284                    preAssignBranch.findFirstToken(assignType);
285            if (assignToken != null) {
286                matchedAssignToken = assignToken;
287                break;
288            }
289        }
290
291        return matchedAssignToken;
292    }
293
294    /**
295     * Checks whether a pattern variable should be declared final.
296     *
297     * @param patternVariableIdent AST ident of the pattern variable
298     * @param reassignedVariableIdents list of AST idents of reassigned vars
299     * @return true if pattern variable should be final, false otherwise
300     */
301    private static boolean shouldBeFinal(
302            final DetailAST patternVariableIdent,
303            final Iterable<DetailAST> reassignedVariableIdents) {
304
305        final DetailAST patternVariableDef = patternVariableIdent.getParent();
306        final DetailAST modifiers =
307                patternVariableDef.findFirstToken(TokenTypes.MODIFIERS);
308        final boolean isFinal =
309                modifiers.findFirstToken(TokenTypes.FINAL) != null;
310        boolean result = false;
311
312        if (!isFinal) {
313            boolean isReassigned = false;
314            for (DetailAST assignTokenIdent : reassignedVariableIdents) {
315                if (patternVariableIdent.getText().equals(
316                        assignTokenIdent.getText())) {
317                    isReassigned = true;
318                    break;
319                }
320            }
321            result = !isReassigned;
322        }
323
324        return result;
325    }
326
327    /**
328     * Finds the nearest AST node that defines the scope in which reassignment
329     * of a pattern variable may occur.
330     *
331     * <p>
332     * The scope is determined by locating the closest enclosing conditional
333     * structure such as {@code if}, {@code else}, or ternary operator.
334     * </p>
335     *
336     * @param ast the AST node to start searching from
337     * @return the AST node representing the reassignment scope root,
338     *         or {@code null} if none is found
339     */
340    @Nullable
341    private static DetailAST findReassignmentScopeRoot(final DetailAST ast) {
342
343        DetailAST result = null;
344
345        for (DetailAST node = ast; node != null && result == null;
346             node = node.getParent()) {
347
348            final int type = node.getType();
349
350            if (type == TokenTypes.LITERAL_IF
351                    || type == TokenTypes.LITERAL_ELSE
352                    || type == TokenTypes.QUESTION) {
353                result = node;
354            }
355        }
356
357        return result;
358    }
359
360    /**
361     * Expands the reassignment scope root into concrete AST branches
362     * that may contain reassigned pattern variables.
363     *
364     * <p>
365     * For ternary expressions, the conditional expression itself is
366     * treated as the reassignment scope. For {@code if} / {@code else}
367     * statements, the method includes the statement list and any
368     * subsequent statements where the pattern variable remains in scope.
369     * </p>
370     *
371     * @param scopeRoot the root AST node of the reassignment scope
372     * @return list of AST branches that may contain reassignments
373     */
374    private static List<DetailAST> expandReassignmentScopes(
375            final DetailAST scopeRoot) {
376
377        final List<DetailAST> branches = new ArrayList<>();
378
379        addBodyBranch(branches, scopeRoot);
380        branches.addAll(getStatementsInExtendedScope(scopeRoot));
381
382        return branches;
383    }
384
385    /**
386     * Adds the body branch of a conditional (if/else) to the list.
387     * For braced blocks, adds the SLIST. For unbraced statements,
388     * adds the single statement directly.
389     *
390     * @param branches collection to add the body branch to
391     * @param scopeRoot the if/else AST node
392     */
393    private static void addBodyBranch(final Collection<DetailAST> branches,
394            final DetailAST scopeRoot) {
395        if (scopeRoot.getType() == TokenTypes.LITERAL_IF) {
396            final DetailAST rparen = scopeRoot.findFirstToken(TokenTypes.RPAREN);
397            branches.add(rparen.getNextSibling());
398        }
399        else {
400            branches.add(scopeRoot);
401        }
402    }
403
404}