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