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 }