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.whitespace;
021
022import java.util.Locale;
023import java.util.function.UnaryOperator;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
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.CommonUtil;
030import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
031
032/**
033 * <div>
034 * Checks the policy on how to wrap lines on
035 * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/opsummary.html">
036 * operators</a>.
037 * </div>
038 *
039 * <p>
040 * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.20.2">
041 * Java Language Specification</a> for more information about {@code instanceof} operator.
042 * </p>
043 *
044 * @since 3.0
045 */
046@StatelessCheck
047public class OperatorWrapCheck
048    extends AbstractCheck {
049
050    /**
051     * A key is pointing to the warning message text in "messages.properties"
052     * file.
053     */
054    public static final String MSG_LINE_NEW = "line.new";
055
056    /**
057     * A key is pointing to the warning message text in "messages.properties"
058     * file.
059     */
060    public static final String MSG_LINE_PREVIOUS = "line.previous";
061
062    /** Specify policy on how to wrap lines. */
063    private WrapOption option = WrapOption.NL;
064
065    /**
066     * Creates a new {@code OperatorWrapCheck} instance.
067     */
068    public OperatorWrapCheck() {
069        // no code by default
070    }
071
072    /**
073     * Setter to specify policy on how to wrap lines.
074     *
075     * @param optionStr string to decode option from
076     * @throws IllegalArgumentException if unable to decode
077     * @since 3.0
078     */
079    public void setOption(String optionStr) {
080        option = WrapOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
081    }
082
083    @Override
084    public int[] getDefaultTokens() {
085        return new int[] {
086            TokenTypes.QUESTION,          // '?'
087            TokenTypes.COLON,             // ':' (not reported for a case)
088            TokenTypes.EQUAL,             // "=="
089            TokenTypes.NOT_EQUAL,         // "!="
090            TokenTypes.DIV,               // '/'
091            TokenTypes.PLUS,              // '+' (unary plus is UNARY_PLUS)
092            TokenTypes.MINUS,             // '-' (unary minus is UNARY_MINUS)
093            TokenTypes.STAR,              // '*'
094            TokenTypes.MOD,               // '%'
095            TokenTypes.SR,                // ">>"
096            TokenTypes.BSR,               // ">>>"
097            TokenTypes.GE,                // ">="
098            TokenTypes.GT,                // ">"
099            TokenTypes.SL,                // "<<"
100            TokenTypes.LE,                // "<="
101            TokenTypes.LT,                // '<'
102            TokenTypes.BXOR,              // '^'
103            TokenTypes.BOR,               // '|'
104            TokenTypes.LOR,               // "||"
105            TokenTypes.BAND,              // '&'
106            TokenTypes.LAND,              // "&&"
107            TokenTypes.TYPE_EXTENSION_AND,
108            TokenTypes.LITERAL_INSTANCEOF,
109        };
110    }
111
112    @Override
113    public int[] getAcceptableTokens() {
114        return new int[] {
115            TokenTypes.QUESTION,          // '?'
116            TokenTypes.COLON,             // ':' (not reported for a case)
117            TokenTypes.EQUAL,             // "=="
118            TokenTypes.NOT_EQUAL,         // "!="
119            TokenTypes.DIV,               // '/'
120            TokenTypes.PLUS,              // '+' (unary plus is UNARY_PLUS)
121            TokenTypes.MINUS,             // '-' (unary minus is UNARY_MINUS)
122            TokenTypes.STAR,              // '*'
123            TokenTypes.MOD,               // '%'
124            TokenTypes.SR,                // ">>"
125            TokenTypes.BSR,               // ">>>"
126            TokenTypes.GE,                // ">="
127            TokenTypes.GT,                // ">"
128            TokenTypes.SL,                // "<<"
129            TokenTypes.LE,                // "<="
130            TokenTypes.LT,                // '<'
131            TokenTypes.BXOR,              // '^'
132            TokenTypes.BOR,               // '|'
133            TokenTypes.LOR,               // "||"
134            TokenTypes.BAND,              // '&'
135            TokenTypes.LAND,              // "&&"
136            TokenTypes.LITERAL_INSTANCEOF,
137            TokenTypes.TYPE_EXTENSION_AND,
138            TokenTypes.ASSIGN,            // '='
139            TokenTypes.DIV_ASSIGN,        // "/="
140            TokenTypes.PLUS_ASSIGN,       // "+="
141            TokenTypes.MINUS_ASSIGN,      // "-="
142            TokenTypes.STAR_ASSIGN,       // "*="
143            TokenTypes.MOD_ASSIGN,        // "%="
144            TokenTypes.SR_ASSIGN,         // ">>="
145            TokenTypes.BSR_ASSIGN,        // ">>>="
146            TokenTypes.SL_ASSIGN,         // "<<="
147            TokenTypes.BXOR_ASSIGN,       // "^="
148            TokenTypes.BOR_ASSIGN,        // "|="
149            TokenTypes.BAND_ASSIGN,       // "&="
150            TokenTypes.METHOD_REF,        // "::"
151            TokenTypes.LAMBDA,            // "->"
152        };
153    }
154
155    @Override
156    public int[] getRequiredTokens() {
157        return CommonUtil.EMPTY_INT_ARRAY;
158    }
159
160    @Override
161    public void visitToken(DetailAST ast) {
162        if (isTargetNode(ast)) {
163            if (option == WrapOption.NL && isNewLineModeViolation(ast)) {
164                log(ast, MSG_LINE_NEW, ast.getText());
165            }
166            else if (option == WrapOption.EOL && isEndOfLineModeViolation(ast)) {
167                log(ast, MSG_LINE_PREVIOUS, ast.getText());
168            }
169        }
170    }
171
172    /**
173     * Filters some false tokens that this check should ignore.
174     *
175     * @param node the node to check
176     * @return {@code true} for all nodes this check should validate
177     */
178    private static boolean isTargetNode(DetailAST node) {
179        final boolean result;
180        if (node.getType() == TokenTypes.COLON) {
181            result = !isColonFromLabel(node);
182        }
183        else if (node.getType() == TokenTypes.STAR) {
184            // Unlike the import statement, the multiply operator always has children
185            result = node.hasChildren();
186        }
187        else {
188            result = true;
189        }
190        return result;
191    }
192
193    /**
194     * Checks whether operator violates {@link WrapOption#NL} mode.
195     *
196     * @param ast the DetailAst of an operator
197     * @return {@code true} if mode does not match
198     */
199    private static boolean isNewLineModeViolation(DetailAST ast) {
200        return TokenUtil.areOnSameLine(ast, getLeftNode(ast))
201                && !TokenUtil.areOnSameLine(ast, getRightNode(ast));
202    }
203
204    /**
205     * Checks whether operator violates {@link WrapOption#EOL} mode.
206     *
207     * @param ast the DetailAst of an operator
208     * @return {@code true} if mode does not match
209     */
210    private static boolean isEndOfLineModeViolation(DetailAST ast) {
211        return !TokenUtil.areOnSameLine(ast, getLeftNode(ast));
212    }
213
214    /**
215     * Checks if a node is {@link TokenTypes#COLON} from a label, switch case of default.
216     *
217     * @param node the node to check
218     * @return {@code true} if node matches
219     */
220    private static boolean isColonFromLabel(DetailAST node) {
221        return TokenUtil.isOfType(node.getParent(), TokenTypes.LABELED_STAT,
222            TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT);
223    }
224
225    /**
226     * Checks if a node is {@link TokenTypes#ASSIGN} to a variable or resource.
227     *
228     * @param node the node to check
229     * @return {@code true} if node matches
230     */
231    private static boolean isAssignToVariable(DetailAST node) {
232        return TokenUtil.isOfType(node.getParent(), TokenTypes.VARIABLE_DEF, TokenTypes.RESOURCE);
233    }
234
235    /**
236     * Returns the left neighbour of a binary operator. This is the rightmost
237     * grandchild of the left child or sibling. For the assign operator the return value is
238     * the variable name.
239     *
240     * @param node the binary operator
241     * @return nearest node from left
242     */
243    private static DetailAST getLeftNode(DetailAST node) {
244        DetailAST result;
245        if (node.getFirstChild() == null || isAssignToVariable(node)) {
246            result = node.getPreviousSibling();
247        }
248        else {
249            result = adjustParens(node.getFirstChild(), DetailAST::getNextSibling);
250        }
251        while (result.getLastChild() != null) {
252            result = result.getLastChild();
253        }
254        return result;
255    }
256
257    /**
258     * Returns the right neighbour of a binary operator. This is the leftmost
259     * grandchild of the right child or sibling. For the ternary operator this
260     * is the node between {@code ?} and {@code :} .
261     *
262     * @param node the binary operator
263     * @return nearest node from right
264     */
265    private static DetailAST getRightNode(DetailAST node) {
266        DetailAST result;
267        if (node.getLastChild() == null) {
268            result = node.getNextSibling();
269        }
270        else {
271            final DetailAST rightNode;
272            if (node.getType() == TokenTypes.QUESTION) {
273                rightNode = node.findFirstToken(TokenTypes.COLON).getPreviousSibling();
274            }
275            else {
276                rightNode = node.getLastChild();
277            }
278            result = adjustParens(rightNode, DetailAST::getPreviousSibling);
279        }
280
281        if (!TokenUtil.isOfType(result, TokenTypes.ARRAY_INIT, TokenTypes.ANNOTATION_ARRAY_INIT)) {
282            while (result.getFirstChild() != null) {
283                result = result.getFirstChild();
284            }
285        }
286        return result;
287    }
288
289    /**
290     * Finds matching parentheses among siblings. If the given node is not
291     * {@link TokenTypes#LPAREN} nor {@link TokenTypes#RPAREN}, the method adjusts nothing.
292     * This method is for handling case like {@code
293     *   (condition && (condition
294     *     || condition2 || condition3) && condition4
295     *     && condition3)
296     * }
297     *
298     * @param node the node to adjust
299     * @param step the node transformer, should be {@link DetailAST#getPreviousSibling}
300     *             or {@link DetailAST#getNextSibling}
301     * @return adjusted node
302     */
303    private static DetailAST adjustParens(DetailAST node, UnaryOperator<DetailAST> step) {
304        DetailAST result = node;
305        int accumulator = 0;
306        while (true) {
307            if (result.getType() == TokenTypes.LPAREN) {
308                accumulator--;
309            }
310            else if (result.getType() == TokenTypes.RPAREN) {
311                accumulator++;
312            }
313            if (accumulator == 0) {
314                break;
315            }
316            result = step.apply(result);
317        }
318        return result;
319    }
320
321}