View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.whitespace;
21  
22  import java.util.Locale;
23  import java.util.function.UnaryOperator;
24  
25  import com.puppycrawl.tools.checkstyle.StatelessCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
31  
32  /**
33   * <div>
34   * Checks the policy on how to wrap lines on
35   * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/opsummary.html">
36   * operators</a>.
37   * </div>
38   *
39   * <p>
40   * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.20.2">
41   * Java Language Specification</a> for more information about {@code instanceof} operator.
42   * </p>
43   *
44   * @since 3.0
45   */
46  @StatelessCheck
47  public class OperatorWrapCheck
48      extends AbstractCheck {
49  
50      /**
51       * A key is pointing to the warning message text in "messages.properties"
52       * file.
53       */
54      public static final String MSG_LINE_NEW = "line.new";
55  
56      /**
57       * A key is pointing to the warning message text in "messages.properties"
58       * file.
59       */
60      public static final String MSG_LINE_PREVIOUS = "line.previous";
61  
62      /** Specify policy on how to wrap lines. */
63      private WrapOption option = WrapOption.NL;
64  
65      /**
66       * Creates a new {@code OperatorWrapCheck} instance.
67       */
68      public OperatorWrapCheck() {
69          // no code by default
70      }
71  
72      /**
73       * Setter to specify policy on how to wrap lines.
74       *
75       * @param optionStr string to decode option from
76       * @throws IllegalArgumentException if unable to decode
77       * @since 3.0
78       */
79      public void setOption(String optionStr) {
80          option = WrapOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
81      }
82  
83      @Override
84      public int[] getDefaultTokens() {
85          return new int[] {
86              TokenTypes.QUESTION,          // '?'
87              TokenTypes.COLON,             // ':' (not reported for a case)
88              TokenTypes.EQUAL,             // "=="
89              TokenTypes.NOT_EQUAL,         // "!="
90              TokenTypes.DIV,               // '/'
91              TokenTypes.PLUS,              // '+' (unary plus is UNARY_PLUS)
92              TokenTypes.MINUS,             // '-' (unary minus is UNARY_MINUS)
93              TokenTypes.STAR,              // '*'
94              TokenTypes.MOD,               // '%'
95              TokenTypes.SR,                // ">>"
96              TokenTypes.BSR,               // ">>>"
97              TokenTypes.GE,                // ">="
98              TokenTypes.GT,                // ">"
99              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 }