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.coding; 021 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.List; 025import java.util.regex.Pattern; 026 027import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 033 034/** 035 * <div> 036 * Checks if unnecessary parentheses are used in a statement or expression. 037 * The check will flag the following with warnings: 038 * </div> 039 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 040 * return (x); // parens around identifier 041 * return (x + 1); // parens around return value 042 * int x = (y / 2 + 1); // parens around assignment rhs 043 * for (int i = (0); i < 10; i++) { // parens around literal 044 * t -= (z + 1); // parens around assignment rhs 045 * boolean a = (x > 7 && y > 5) // parens around expression 046 * || z < 9; 047 * boolean b = (~a) > -27 // parens around ~a 048 * && (a-- < 30); // parens around expression 049 * </code></pre></div> 050 * 051 * <p> 052 * Notes: 053 * The check is not "type aware", that is to say, it can't tell if parentheses 054 * are unnecessary based on the types in an expression. The check is partially aware about 055 * operator precedence but unaware about operator associativity. 056 * It won't catch cases such as: 057 * </p> 058 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 059 * int x = (a + b) + c; // 1st Case 060 * boolean p = true; // 2nd Case 061 * int q = 4; 062 * int r = 3; 063 * if (p == (q <= r)) {} 064 * </code></pre></div> 065 * 066 * <p> 067 * In the first case, given that <em>a</em>, <em>b</em>, and <em>c</em> are 068 * all {@code int} variables, the parentheses around {@code a + b} 069 * are not needed. 070 * In the second case, parentheses are required as <em>q</em>, <em>r</em> are 071 * of type {@code int} and <em>p</em> is of type {@code boolean} 072 * and removing parentheses will give a compile-time error. Even if <em>q</em> 073 * and <em>r</em> were {@code boolean} still there will be no violation 074 * raised as check is not "type aware". 075 * </p> 076 * 077 * <p> 078 * The partial support for operator precedence includes cases of the following type: 079 * </p> 080 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 081 * boolean a = true, b = true; 082 * boolean c = false, d = false; 083 * if ((a && b) || c) { // violation, unnecessary paren 084 * } 085 * if (a && (b || c)) { // ok 086 * } 087 * if ((a == b) && c) { // violation, unnecessary paren 088 * } 089 * String e = "e"; 090 * if ((e instanceof String) && a || b) { // violation, unnecessary paren 091 * } 092 * int f = 0; 093 * int g = 0; 094 * if (!(f >= g) // ok 095 * && (g > f)) { // violation, unnecessary paren 096 * } 097 * if ((++f) > g && a) { // violation, unnecessary paren 098 * } 099 * </code></pre></div> 100 * 101 * @since 3.4 102 */ 103@FileStatefulCheck 104public class UnnecessaryParenthesesCheck extends AbstractCheck { 105 106 /** 107 * A key is pointing to the warning message text in "messages.properties" 108 * file. 109 */ 110 public static final String MSG_IDENT = "unnecessary.paren.ident"; 111 112 /** 113 * A key is pointing to the warning message text in "messages.properties" 114 * file. 115 */ 116 public static final String MSG_ASSIGN = "unnecessary.paren.assign"; 117 118 /** 119 * A key is pointing to the warning message text in "messages.properties" 120 * file. 121 */ 122 public static final String MSG_EXPR = "unnecessary.paren.expr"; 123 124 /** 125 * A key is pointing to the warning message text in "messages.properties" 126 * file. 127 */ 128 public static final String MSG_LITERAL = "unnecessary.paren.literal"; 129 130 /** 131 * A key is pointing to the warning message text in "messages.properties" 132 * file. 133 */ 134 public static final String MSG_STRING = "unnecessary.paren.string"; 135 136 /** 137 * A key is pointing to the warning message text in "messages.properties" 138 * file. 139 */ 140 public static final String MSG_RETURN = "unnecessary.paren.return"; 141 142 /** 143 * A key is pointing to the warning message text in "messages.properties" 144 * file. 145 */ 146 public static final String MSG_LAMBDA = "unnecessary.paren.lambda"; 147 148 /** 149 * Compiled pattern used to match newline control characters, for replacement. 150 */ 151 private static final Pattern NEWLINE = Pattern.compile("\\R"); 152 153 /** 154 * String used to amend TEXT_BLOCK_CONTENT so that it matches STRING_LITERAL. 155 */ 156 private static final String QUOTE = "\""; 157 158 /** The maximum string length before we chop the string. */ 159 private static final int MAX_QUOTED_LENGTH = 25; 160 161 /** Token types for literals. */ 162 private static final int[] LITERALS = { 163 TokenTypes.NUM_DOUBLE, 164 TokenTypes.NUM_FLOAT, 165 TokenTypes.NUM_INT, 166 TokenTypes.NUM_LONG, 167 TokenTypes.STRING_LITERAL, 168 TokenTypes.LITERAL_NULL, 169 TokenTypes.LITERAL_FALSE, 170 TokenTypes.LITERAL_TRUE, 171 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 172 }; 173 174 /** Token types for assignment operations. */ 175 private static final int[] ASSIGNMENTS = { 176 TokenTypes.ASSIGN, 177 TokenTypes.BAND_ASSIGN, 178 TokenTypes.BOR_ASSIGN, 179 TokenTypes.BSR_ASSIGN, 180 TokenTypes.BXOR_ASSIGN, 181 TokenTypes.DIV_ASSIGN, 182 TokenTypes.MINUS_ASSIGN, 183 TokenTypes.MOD_ASSIGN, 184 TokenTypes.PLUS_ASSIGN, 185 TokenTypes.SL_ASSIGN, 186 TokenTypes.SR_ASSIGN, 187 TokenTypes.STAR_ASSIGN, 188 }; 189 190 /** Token types for conditional operators. */ 191 private static final int[] CONDITIONAL_OPERATOR = { 192 TokenTypes.LOR, 193 TokenTypes.LAND, 194 }; 195 196 /** Token types for relation operator. */ 197 private static final int[] RELATIONAL_OPERATOR = { 198 TokenTypes.LITERAL_INSTANCEOF, 199 TokenTypes.GT, 200 TokenTypes.LT, 201 TokenTypes.GE, 202 TokenTypes.LE, 203 TokenTypes.EQUAL, 204 TokenTypes.NOT_EQUAL, 205 }; 206 207 /** Token types for unary and postfix operators. */ 208 private static final int[] UNARY_AND_POSTFIX = { 209 TokenTypes.UNARY_MINUS, 210 TokenTypes.UNARY_PLUS, 211 TokenTypes.INC, 212 TokenTypes.DEC, 213 TokenTypes.LNOT, 214 TokenTypes.BNOT, 215 TokenTypes.POST_INC, 216 TokenTypes.POST_DEC, 217 }; 218 219 /** Token types for bitwise binary operator. */ 220 private static final int[] BITWISE_BINARY_OPERATORS = { 221 TokenTypes.BXOR, 222 TokenTypes.BOR, 223 TokenTypes.BAND, 224 }; 225 226 /** 227 * Used to test if logging a warning in a parent node may be skipped 228 * because a warning was already logged on an immediate child node. 229 */ 230 private DetailAST parentToSkip; 231 /** Depth of nested assignments. Normally this will be 0 or 1. */ 232 private int assignDepth; 233 234 /** 235 * Creates a new {@code UnnecessaryParenthesesCheck} instance. 236 */ 237 public UnnecessaryParenthesesCheck() { 238 // no code by default 239 } 240 241 @Override 242 public int[] getDefaultTokens() { 243 return new int[] { 244 TokenTypes.EXPR, 245 TokenTypes.IDENT, 246 TokenTypes.NUM_DOUBLE, 247 TokenTypes.NUM_FLOAT, 248 TokenTypes.NUM_INT, 249 TokenTypes.NUM_LONG, 250 TokenTypes.STRING_LITERAL, 251 TokenTypes.LITERAL_NULL, 252 TokenTypes.LITERAL_FALSE, 253 TokenTypes.LITERAL_TRUE, 254 TokenTypes.ASSIGN, 255 TokenTypes.BAND_ASSIGN, 256 TokenTypes.BOR_ASSIGN, 257 TokenTypes.BSR_ASSIGN, 258 TokenTypes.BXOR_ASSIGN, 259 TokenTypes.DIV_ASSIGN, 260 TokenTypes.MINUS_ASSIGN, 261 TokenTypes.MOD_ASSIGN, 262 TokenTypes.PLUS_ASSIGN, 263 TokenTypes.SL_ASSIGN, 264 TokenTypes.SR_ASSIGN, 265 TokenTypes.STAR_ASSIGN, 266 TokenTypes.LAMBDA, 267 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 268 TokenTypes.LAND, 269 TokenTypes.LOR, 270 TokenTypes.LITERAL_INSTANCEOF, 271 TokenTypes.GT, 272 TokenTypes.LT, 273 TokenTypes.GE, 274 TokenTypes.LE, 275 TokenTypes.EQUAL, 276 TokenTypes.NOT_EQUAL, 277 TokenTypes.UNARY_MINUS, 278 TokenTypes.UNARY_PLUS, 279 TokenTypes.INC, 280 TokenTypes.DEC, 281 TokenTypes.LNOT, 282 TokenTypes.BNOT, 283 TokenTypes.POST_INC, 284 TokenTypes.POST_DEC, 285 }; 286 } 287 288 @Override 289 public int[] getAcceptableTokens() { 290 return new int[] { 291 TokenTypes.EXPR, 292 TokenTypes.IDENT, 293 TokenTypes.NUM_DOUBLE, 294 TokenTypes.NUM_FLOAT, 295 TokenTypes.NUM_INT, 296 TokenTypes.NUM_LONG, 297 TokenTypes.STRING_LITERAL, 298 TokenTypes.LITERAL_NULL, 299 TokenTypes.LITERAL_FALSE, 300 TokenTypes.LITERAL_TRUE, 301 TokenTypes.ASSIGN, 302 TokenTypes.BAND_ASSIGN, 303 TokenTypes.BOR_ASSIGN, 304 TokenTypes.BSR_ASSIGN, 305 TokenTypes.BXOR_ASSIGN, 306 TokenTypes.DIV_ASSIGN, 307 TokenTypes.MINUS_ASSIGN, 308 TokenTypes.MOD_ASSIGN, 309 TokenTypes.PLUS_ASSIGN, 310 TokenTypes.SL_ASSIGN, 311 TokenTypes.SR_ASSIGN, 312 TokenTypes.STAR_ASSIGN, 313 TokenTypes.LAMBDA, 314 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 315 TokenTypes.LAND, 316 TokenTypes.LOR, 317 TokenTypes.LITERAL_INSTANCEOF, 318 TokenTypes.GT, 319 TokenTypes.LT, 320 TokenTypes.GE, 321 TokenTypes.LE, 322 TokenTypes.EQUAL, 323 TokenTypes.NOT_EQUAL, 324 TokenTypes.UNARY_MINUS, 325 TokenTypes.UNARY_PLUS, 326 TokenTypes.INC, 327 TokenTypes.DEC, 328 TokenTypes.LNOT, 329 TokenTypes.BNOT, 330 TokenTypes.POST_INC, 331 TokenTypes.POST_DEC, 332 TokenTypes.BXOR, 333 TokenTypes.BOR, 334 TokenTypes.BAND, 335 TokenTypes.QUESTION, 336 }; 337 } 338 339 @Override 340 public int[] getRequiredTokens() { 341 // Check can work with any of acceptable tokens 342 return CommonUtil.EMPTY_INT_ARRAY; 343 } 344 345 // -@cs[CyclomaticComplexity] All logs should be in visit token. 346 @Override 347 public void visitToken(DetailAST ast) { 348 final DetailAST parent = ast.getParent(); 349 350 if (isLambdaSingleParameterSurrounded(ast)) { 351 log(ast, MSG_LAMBDA); 352 } 353 else if (ast.getType() == TokenTypes.QUESTION) { 354 getParenthesesChildrenAroundQuestion(ast) 355 .forEach(unnecessaryChild -> log(unnecessaryChild, MSG_EXPR)); 356 } 357 else if (parent.getType() != TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) { 358 final int type = ast.getType(); 359 final boolean surrounded = isSurrounded(ast); 360 // An identifier surrounded by parentheses. 361 if (surrounded && type == TokenTypes.IDENT) { 362 parentToSkip = ast.getParent(); 363 log(ast, MSG_IDENT, ast.getText()); 364 } 365 // A literal (numeric or string) surrounded by parentheses. 366 else if (surrounded && TokenUtil.isOfType(type, LITERALS)) { 367 parentToSkip = ast.getParent(); 368 if (type == TokenTypes.STRING_LITERAL) { 369 log(ast, MSG_STRING, 370 chopString(ast.getText())); 371 } 372 else if (type == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN) { 373 // Strip newline control characters to keep message as single-line, add 374 // quotes to make string consistent with STRING_LITERAL 375 final String logString = QUOTE 376 + NEWLINE.matcher( 377 ast.getFirstChild().getText()).replaceAll("\\\\n") 378 + QUOTE; 379 log(ast, MSG_STRING, chopString(logString)); 380 } 381 else { 382 log(ast, MSG_LITERAL, ast.getText()); 383 } 384 } 385 // The rhs of an assignment surrounded by parentheses. 386 else if (TokenUtil.isOfType(type, ASSIGNMENTS)) { 387 assignDepth++; 388 final DetailAST last = ast.getLastChild(); 389 if (last.getType() == TokenTypes.RPAREN) { 390 log(ast, MSG_ASSIGN); 391 } 392 } 393 } 394 } 395 396 @Override 397 public void leaveToken(DetailAST ast) { 398 final int type = ast.getType(); 399 final DetailAST parent = ast.getParent(); 400 401 // shouldn't process assign in annotation pairs 402 if (type != TokenTypes.ASSIGN 403 || parent.getType() != TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) { 404 if (type == TokenTypes.EXPR) { 405 checkExpression(ast); 406 } 407 else if (TokenUtil.isOfType(type, ASSIGNMENTS)) { 408 assignDepth--; 409 } 410 else if (isSurrounded(ast) && unnecessaryParenAroundOperators(ast)) { 411 log(ast.getPreviousSibling(), MSG_EXPR); 412 } 413 } 414 } 415 416 /** 417 * Tests if the given {@code DetailAST} is surrounded by parentheses. 418 * 419 * @param ast the {@code DetailAST} to check if it is surrounded by 420 * parentheses. 421 * @return {@code true} if {@code ast} is surrounded by 422 * parentheses. 423 */ 424 private static boolean isSurrounded(DetailAST ast) { 425 final DetailAST prev = ast.getPreviousSibling(); 426 final DetailAST parent = ast.getParent(); 427 final boolean isPreviousSiblingLeftParenthesis = prev != null 428 && prev.getType() == TokenTypes.LPAREN; 429 final boolean isMethodCallWithUnnecessaryParenthesis = 430 parent.getType() == TokenTypes.METHOD_CALL 431 && parent.getPreviousSibling() != null 432 && parent.getPreviousSibling().getType() == TokenTypes.LPAREN; 433 return isPreviousSiblingLeftParenthesis || isMethodCallWithUnnecessaryParenthesis; 434 } 435 436 /** 437 * Tests if the given expression node is surrounded by parentheses. 438 * 439 * @param ast a {@code DetailAST} whose type is 440 * {@code TokenTypes.EXPR}. 441 * @return {@code true} if the expression is surrounded by 442 * parentheses. 443 */ 444 private static boolean isExprSurrounded(DetailAST ast) { 445 return ast.getFirstChild().getType() == TokenTypes.LPAREN; 446 } 447 448 /** 449 * Checks whether an expression is surrounded by parentheses. 450 * 451 * @param ast the {@code DetailAST} to check if it is surrounded by 452 * parentheses. 453 */ 454 private void checkExpression(DetailAST ast) { 455 // If 'parentToSkip' == 'ast', then we've already logged a 456 // warning about an immediate child node in visitToken, so we don't 457 // need to log another one here. 458 if (parentToSkip != ast && isExprSurrounded(ast)) { 459 if (ast.getParent().getType() == TokenTypes.LITERAL_RETURN) { 460 log(ast, MSG_RETURN); 461 } 462 else if (assignDepth >= 1) { 463 log(ast, MSG_ASSIGN); 464 } 465 else { 466 log(ast, MSG_EXPR); 467 } 468 } 469 } 470 471 /** 472 * Checks if conditional, relational, bitwise binary operator, unary and postfix operators 473 * in expressions are surrounded by unnecessary parentheses. 474 * 475 * @param ast the {@code DetailAST} to check if it is surrounded by 476 * unnecessary parentheses. 477 * @return {@code true} if the expression is surrounded by 478 * unnecessary parentheses. 479 */ 480 private static boolean unnecessaryParenAroundOperators(DetailAST ast) { 481 final int type = ast.getType(); 482 final boolean isConditionalOrRelational = TokenUtil.isOfType(type, CONDITIONAL_OPERATOR) 483 || TokenUtil.isOfType(type, RELATIONAL_OPERATOR); 484 final boolean isBitwise = TokenUtil.isOfType(type, BITWISE_BINARY_OPERATORS); 485 final boolean hasUnnecessaryParentheses; 486 if (isConditionalOrRelational) { 487 hasUnnecessaryParentheses = checkConditionalOrRelationalOperator(ast); 488 } 489 else if (isBitwise) { 490 hasUnnecessaryParentheses = checkBitwiseBinaryOperator(ast); 491 } 492 else { 493 hasUnnecessaryParentheses = TokenUtil.isOfType(type, UNARY_AND_POSTFIX) 494 && isBitWiseBinaryOrConditionalOrRelationalOperator(ast.getParent().getType()); 495 } 496 return hasUnnecessaryParentheses; 497 } 498 499 /** 500 * Check if conditional or relational operator has unnecessary parentheses. 501 * 502 * @param ast to check if surrounded by unnecessary parentheses 503 * @return true if unnecessary parenthesis 504 */ 505 private static boolean checkConditionalOrRelationalOperator(DetailAST ast) { 506 final int type = ast.getType(); 507 final int parentType = ast.getParent().getType(); 508 final boolean isParentEqualityOperator = 509 TokenUtil.isOfType(parentType, TokenTypes.EQUAL, TokenTypes.NOT_EQUAL); 510 final boolean result; 511 if (type == TokenTypes.LOR) { 512 result = !TokenUtil.isOfType(parentType, TokenTypes.LAND) 513 && !TokenUtil.isOfType(parentType, BITWISE_BINARY_OPERATORS); 514 } 515 else if (type == TokenTypes.LAND) { 516 result = !TokenUtil.isOfType(parentType, BITWISE_BINARY_OPERATORS); 517 } 518 else { 519 result = true; 520 } 521 return result && !isParentEqualityOperator 522 && isBitWiseBinaryOrConditionalOrRelationalOperator(parentType); 523 } 524 525 /** 526 * Check if bitwise binary operator has unnecessary parentheses. 527 * 528 * @param ast to check if surrounded by unnecessary parentheses 529 * @return true if unnecessary parenthesis 530 */ 531 private static boolean checkBitwiseBinaryOperator(DetailAST ast) { 532 final int type = ast.getType(); 533 final int parentType = ast.getParent().getType(); 534 final boolean result; 535 if (type == TokenTypes.BOR) { 536 result = !TokenUtil.isOfType(parentType, TokenTypes.BAND, TokenTypes.BXOR) 537 && !TokenUtil.isOfType(parentType, RELATIONAL_OPERATOR); 538 } 539 else if (type == TokenTypes.BXOR) { 540 result = !TokenUtil.isOfType(parentType, TokenTypes.BAND) 541 && !TokenUtil.isOfType(parentType, RELATIONAL_OPERATOR); 542 } 543 // we deal with bitwise AND here. 544 else { 545 result = !TokenUtil.isOfType(parentType, RELATIONAL_OPERATOR); 546 } 547 return result && isBitWiseBinaryOrConditionalOrRelationalOperator(parentType); 548 } 549 550 /** 551 * Check if token type is bitwise binary or conditional or relational operator. 552 * 553 * @param type Token type to check 554 * @return true if it is bitwise binary or conditional operator 555 */ 556 private static boolean isBitWiseBinaryOrConditionalOrRelationalOperator(int type) { 557 return TokenUtil.isOfType(type, CONDITIONAL_OPERATOR) 558 || TokenUtil.isOfType(type, RELATIONAL_OPERATOR) 559 || TokenUtil.isOfType(type, BITWISE_BINARY_OPERATORS); 560 } 561 562 /** 563 * Tests if the given node has a single parameter, no defined type, and is surrounded 564 * by parentheses. This condition can only be true for lambdas. 565 * 566 * @param ast a {@code DetailAST} node 567 * @return {@code true} if the lambda has a single parameter, no defined type, and is 568 * surrounded by parentheses. 569 */ 570 private static boolean isLambdaSingleParameterSurrounded(DetailAST ast) { 571 final DetailAST firstChild = ast.getFirstChild(); 572 boolean result = false; 573 if (TokenUtil.isOfType(firstChild, TokenTypes.LPAREN)) { 574 final DetailAST parameters = firstChild.getNextSibling(); 575 if (parameters.getChildCount(TokenTypes.PARAMETER_DEF) == 1 576 && !parameters.getFirstChild().findFirstToken(TokenTypes.TYPE).hasChildren()) { 577 result = true; 578 } 579 } 580 return result; 581 } 582 583 /** 584 * Returns the direct LPAREN tokens children to a given QUESTION token which 585 * contain an expression not a literal variable. 586 * 587 * @param questionToken {@code DetailAST} question token to be checked 588 * @return the direct children to the given question token which their types are LPAREN 589 * tokens and not contain a literal inside the parentheses 590 */ 591 private static List<DetailAST> getParenthesesChildrenAroundQuestion(DetailAST questionToken) { 592 final List<DetailAST> surroundedChildren = new ArrayList<>(); 593 DetailAST directChild = questionToken.getFirstChild(); 594 while (directChild != null) { 595 if (directChild.getType() == TokenTypes.LPAREN 596 && !TokenUtil.isOfType(directChild.getNextSibling(), LITERALS)) { 597 surroundedChildren.add(directChild); 598 } 599 directChild = directChild.getNextSibling(); 600 } 601 return Collections.unmodifiableList(surroundedChildren); 602 } 603 604 /** 605 * Returns the specified string chopped to {@code MAX_QUOTED_LENGTH} 606 * plus an ellipsis (...) if the length of the string exceeds {@code 607 * MAX_QUOTED_LENGTH}. 608 * 609 * @param value the string to potentially chop. 610 * @return the chopped string if {@code string} is longer than 611 * {@code MAX_QUOTED_LENGTH}; otherwise {@code string}. 612 */ 613 private static String chopString(String value) { 614 String result = value; 615 if (value.length() > MAX_QUOTED_LENGTH) { 616 result = value.substring(0, MAX_QUOTED_LENGTH) + "...\""; 617 } 618 return result; 619 } 620 621}