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}