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.BitSet;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
030
031/**
032 * <div>
033 * Checks for assignments in subexpressions, such as in
034 * {@code String s = Integer.toString(i = 2);}.
035 * </div>
036 *
037 * <p>
038 * Rationale: Except for the loop idioms,
039 * all assignments should occur in their own top-level statement to increase readability.
040 * With inner assignments like the one given above, it is difficult to see all places
041 * where a variable is set.
042 * </p>
043 *
044 * <p>
045 * Note: Check allows usage of the popular assignments in loops:
046 * </p>
047 * {@snippet :
048 * String line;
049 * while ((line = bufferedReader.readLine()) != null) { // OK
050 *   // process the line
051 * }
052 *
053 * for (;(line = bufferedReader.readLine()) != null;) { // OK
054 *   // process the line
055 * }
056 *
057 * do {
058 *   // process the line
059 * }
060 * while ((line = bufferedReader.readLine()) != null); // OK
061 * }
062 *
063 * <p>
064 * Assignment inside a condition is not a problem here, as the assignment is surrounded
065 * by an extra pair of parentheses. The comparison is {@code != null} and there is no chance that
066 * intention was to write {@code line == reader.readLine()}.
067 * </p>
068 *
069 * @since 3.0
070 */
071@StatelessCheck
072public class InnerAssignmentCheck extends AbstractCheck {
073
074    /**
075     * A key is pointing to the warning message text in "messages.properties" file.
076     */
077    public static final String MSG_KEY = "assignment.inner.avoid";
078
079    /**
080     * Allowed AST types from an assignment AST node towards the root.
081     */
082    private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = {
083        {TokenTypes.EXPR, TokenTypes.SLIST},
084        {TokenTypes.VARIABLE_DEF},
085        {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT},
086        {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR},
087        {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, {
088            TokenTypes.RESOURCE,
089            TokenTypes.RESOURCES,
090            TokenTypes.RESOURCE_SPECIFICATION,
091        },
092        {TokenTypes.EXPR, TokenTypes.LAMBDA},
093        {TokenTypes.EXPR, TokenTypes.SWITCH_RULE, TokenTypes.LITERAL_SWITCH, TokenTypes.SLIST},
094    };
095
096    /**
097     * Allowed AST types from an assignment AST node towards the root.
098     */
099    private static final int[][] CONTROL_CONTEXT = {
100        {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
101        {TokenTypes.EXPR, TokenTypes.LITERAL_FOR},
102        {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
103        {TokenTypes.EXPR, TokenTypes.LITERAL_IF},
104        {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE},
105    };
106
107    /**
108     * Allowed AST types from a comparison node (above an assignment) towards the root.
109     */
110    private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = {
111        {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
112        {TokenTypes.EXPR, TokenTypes.FOR_CONDITION},
113        {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
114    };
115
116    /**
117     * The token types that identify comparison operators.
118     */
119    private static final BitSet COMPARISON_TYPES = TokenUtil.asBitSet(
120        TokenTypes.EQUAL,
121        TokenTypes.GE,
122        TokenTypes.GT,
123        TokenTypes.LE,
124        TokenTypes.LT,
125        TokenTypes.NOT_EQUAL
126    );
127
128    /**
129     * The token types that are ignored while checking "loop-idiom".
130     */
131    private static final BitSet LOOP_IDIOM_IGNORED_PARENTS = TokenUtil.asBitSet(
132        TokenTypes.LAND,
133        TokenTypes.LOR,
134        TokenTypes.LNOT,
135        TokenTypes.BOR,
136        TokenTypes.BAND
137    );
138
139    /**
140     * Creates a new {@code InnerAssignmentCheck} instance.
141     */
142    public InnerAssignmentCheck() {
143        // no code by default
144    }
145
146    @Override
147    public int[] getDefaultTokens() {
148        return getRequiredTokens();
149    }
150
151    @Override
152    public int[] getAcceptableTokens() {
153        return getRequiredTokens();
154    }
155
156    @Override
157    public int[] getRequiredTokens() {
158        return new int[] {
159            TokenTypes.ASSIGN,            // '='
160            TokenTypes.DIV_ASSIGN,        // "/="
161            TokenTypes.PLUS_ASSIGN,       // "+="
162            TokenTypes.MINUS_ASSIGN,      // "-="
163            TokenTypes.STAR_ASSIGN,       // "*="
164            TokenTypes.MOD_ASSIGN,        // "%="
165            TokenTypes.SR_ASSIGN,         // ">>="
166            TokenTypes.BSR_ASSIGN,        // ">>>="
167            TokenTypes.SL_ASSIGN,         // "<<="
168            TokenTypes.BXOR_ASSIGN,       // "^="
169            TokenTypes.BOR_ASSIGN,        // "|="
170            TokenTypes.BAND_ASSIGN,       // "&="
171        };
172    }
173
174    @Override
175    public void visitToken(DetailAST ast) {
176        if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT, CommonUtil.EMPTY_BIT_SET)
177                && !isInNoBraceControlStatement(ast)
178                && !isInLoopIdiom(ast)) {
179            log(ast, MSG_KEY);
180        }
181    }
182
183    /**
184     * Determines if ast is in the body of a flow control statement without
185     * braces. An example of such a statement would be
186     * {@snippet :
187     * if (y > 0)
188     *     x = y;
189     * }
190     *
191     * <p>
192     * This leads to the following AST structure:
193     * </p>
194     * {@snippet lang="text" :
195     * LITERAL_IF
196     *     LPAREN
197     *     EXPR // test
198     *     RPAREN
199     *     EXPR // body
200     *     SEMI
201     * }
202     *
203     * <p>
204     * We need to ensure that ast is in the body and not in the test.
205     * </p>
206     *
207     * @param ast an assignment operator AST
208     * @return whether ast is in the body of a flow control statement
209     */
210    private static boolean isInNoBraceControlStatement(DetailAST ast) {
211        boolean result = false;
212        if (isInContext(ast, CONTROL_CONTEXT, CommonUtil.EMPTY_BIT_SET)) {
213            final DetailAST expr = ast.getParent();
214            final DetailAST exprNext = expr.getNextSibling();
215            result = exprNext.getType() == TokenTypes.SEMI;
216        }
217        return result;
218    }
219
220    /**
221     * Tests whether the given AST is used in the "assignment in loop" idiom.
222     * {@snippet :
223     * String line;
224     * while ((line = bufferedReader.readLine()) != null) {
225     *   // process the line
226     * }
227     * for (;(line = bufferedReader.readLine()) != null;) {
228     *   // process the line
229     * }
230     * do {
231     *   // process the line
232     * }
233     * while ((line = bufferedReader.readLine()) != null);
234     * }
235     * Assignment inside a condition is not a problem here, as the assignment is surrounded by an
236     * extra pair of parentheses. The comparison is {@code != null} and there is no chance that
237     * intention was to write {@code line == reader.readLine()}.
238     *
239     * @param ast assignment AST
240     * @return whether the context of the assignment AST indicates the idiom
241     */
242    private static boolean isInLoopIdiom(DetailAST ast) {
243        return isComparison(ast.getParent())
244                    && isInContext(ast.getParent(),
245                            ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT,
246                            LOOP_IDIOM_IGNORED_PARENTS);
247    }
248
249    /**
250     * Checks if an AST is a comparison operator.
251     *
252     * @param ast the AST to check
253     * @return true iff ast is a comparison operator.
254     */
255    private static boolean isComparison(DetailAST ast) {
256        final int astType = ast.getType();
257        return COMPARISON_TYPES.get(astType);
258    }
259
260    /**
261     * Tests whether the provided AST is in one of the given contexts.
262     *
263     * @param ast the AST from which to start walking towards root
264     * @param contextSet the contexts to test against.
265     * @param skipTokens parent token types to ignore
266     *
267     * @return whether the parents nodes of ast match one of the allowed type paths.
268     */
269    private static boolean isInContext(DetailAST ast, int[][] contextSet, BitSet skipTokens) {
270        boolean found = false;
271        for (int[] element : contextSet) {
272            DetailAST current = ast;
273            for (int anElement : element) {
274                current = getParent(current, skipTokens);
275                if (current.getType() == anElement) {
276                    found = true;
277                }
278                else {
279                    found = false;
280                    break;
281                }
282            }
283
284            if (found) {
285                break;
286            }
287        }
288        return found;
289    }
290
291    /**
292     * Get ast parent, ignoring token types from {@code skipTokens}.
293     *
294     * @param ast token to get parent
295     * @param skipTokens token types to skip
296     * @return first not ignored parent of ast
297     */
298    private static DetailAST getParent(DetailAST ast, BitSet skipTokens) {
299        DetailAST result = ast.getParent();
300        while (skipTokens.get(result.getType())) {
301            result = result.getParent();
302        }
303        return result;
304    }
305
306}