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