001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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.imports; 021 022import java.util.Locale; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.FullIdent; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032 033/** 034 * Checks the ordering/grouping of imports. Features are: 035 * <ul> 036 * <li>groups imports: ensures that groups of imports come in a specific order 037 * (e.g., java. comes first, javax. comes second, then everything else)</li> 038 * <li>adds a separation between groups : ensures that a blank line sit between 039 * each group</li> 040 * <li>import groups aren't separated internally: ensures that 041 * each group aren't separated internally by blank line or comment</li> 042 * <li>sorts imports inside each group: ensures that imports within each group 043 * are in lexicographic order</li> 044 * <li>sorts according to case: ensures that the comparison between import is 045 * case sensitive</li> 046 * <li>groups static imports: ensures that static imports are at the top (or the 047 * bottom) of all the imports, or above (or under) each group, or are treated 048 * like non static imports (@see {@link ImportOrderOption}</li> 049 * </ul>. 050 * 051 * <pre> 052 * Properties: 053 * </pre> 054 * <table summary="Properties" border="1"> 055 * <tr><th>name</th><th>Description</th><th>type</th><th>default value</th></tr> 056 * <tr><td>option</td><td>policy on the relative order between regular imports and static 057 * imports</td><td>{@link ImportOrderOption}</td><td>under</td></tr> 058 * <tr><td>groups</td><td>list of imports groups (every group identified either by a common 059 * prefix string, or by a regular expression enclosed in forward slashes (e.g. /regexp/)</td> 060 * <td>list of strings</td><td>empty list</td></tr> 061 * <tr><td>ordered</td><td>whether imports within group should be sorted</td> 062 * <td>Boolean</td><td>true</td></tr> 063 * <tr><td>separated</td><td>whether imports groups should be separated by, at least, 064 * one blank line or comment and aren't separated internally 065 * </td><td>Boolean</td><td>false</td></tr> 066 * <tr><td>caseSensitive</td><td>whether string comparison should be case sensitive or not. 067 * Case sensitive sorting is in ASCII sort order</td><td>Boolean</td><td>true</td></tr> 068 * <tr><td>sortStaticImportsAlphabetically</td><td>whether static imports grouped by top or 069 * bottom option are sorted alphabetically or not</td><td>Boolean</td><td>false</td></tr> 070 * <tr><td>useContainerOrderingForStatic</td><td>whether to use container ordering 071 * (Eclipse IDE term) for static imports or not</td><td>Boolean</td><td>false</td></tr> 072 * </table> 073 * 074 * <p> 075 * Example: 076 * </p> 077 * <p>To configure the check so that it matches default Eclipse formatter configuration 078 * (tested on Kepler, Luna and Mars):</p> 079 * <ul> 080 * <li>group of static imports is on the top</li> 081 * <li>groups of non-static imports: "java" then "javax" 082 * packages first, then "org" and then all other imports</li> 083 * <li>imports will be sorted in the groups</li> 084 * <li>groups are separated by, at least, one blank line and aren't separated internally</li> 085 * </ul> 086 * 087 * <pre> 088 * <module name="ImportOrder"> 089 * <property name="groups" value="/^javax?\./,org"/> 090 * <property name="ordered" value="true"/> 091 * <property name="separated" value="true"/> 092 * <property name="option" value="above"/> 093 * <property name="sortStaticImportsAlphabetically" value="true"/> 094 * </module> 095 * </pre> 096 * 097 * <p>To configure the check so that it matches default IntelliJ IDEA formatter configuration 098 * (tested on v14):</p> 099 * <ul> 100 * <li>group of static imports is on the bottom</li> 101 * <li>groups of non-static imports: all imports except of "javax" and 102 * "java", then "javax" and "java"</li> 103 * <li>imports will be sorted in the groups</li> 104 * <li>groups are separated by, at least, one blank line and aren't separated internally</li> 105 * </ul> 106 * 107 * <p> 108 * Note: "separated" option is disabled because IDEA default has blank line 109 * between "java" and static imports, and no blank line between 110 * "javax" and "java" 111 * </p> 112 * 113 * <pre> 114 * <module name="ImportOrder"> 115 * <property name="groups" value="*,javax,java"/> 116 * <property name="ordered" value="true"/> 117 * <property name="separated" value="false"/> 118 * <property name="option" value="bottom"/> 119 * <property name="sortStaticImportsAlphabetically" value="true"/> 120 * </module> 121 * </pre> 122 * 123 * <p>To configure the check so that it matches default NetBeans formatter configuration 124 * (tested on v8):</p> 125 * <ul> 126 * <li>groups of non-static imports are not defined, all imports will be sorted 127 * as a one group</li> 128 * <li>static imports are not separated, they will be sorted along with other imports</li> 129 * </ul> 130 * 131 * <pre> 132 * <module name="ImportOrder"> 133 * <property name="option" value="inflow"/> 134 * </module> 135 * </pre> 136 * 137 * <p> 138 * Group descriptions enclosed in slashes are interpreted as regular 139 * expressions. If multiple groups match, the one matching a longer 140 * substring of the imported name will take precedence, with ties 141 * broken first in favor of earlier matches and finally in favor of 142 * the first matching group. 143 * </p> 144 * 145 * <p> 146 * There is always a wildcard group to which everything not in a named group 147 * belongs. If an import does not match a named group, the group belongs to 148 * this wildcard group. The wildcard group position can be specified using the 149 * {@code *} character. 150 * </p> 151 * 152 * <p>Check also has on option making it more flexible: 153 * <b>sortStaticImportsAlphabetically</b> - sets whether static imports grouped by 154 * <b>top</b> or <b>bottom</b> option should be sorted alphabetically or 155 * not, default value is <b>false</b>. It is applied to static imports grouped 156 * with <b>top</b> or <b>bottom</b> options.<br> 157 * This option is helping in reconciling of this Check and other tools like 158 * Eclipse's Organize Imports feature. 159 * </p> 160 * <p> 161 * To configure the Check allows static imports grouped to the <b>top</b> 162 * being sorted alphabetically: 163 * </p> 164 * 165 * <pre> 166 * {@code 167 * import static java.lang.Math.abs; 168 * import static org.abego.treelayout.Configuration.AlignmentInLevel; // OK, alphabetical order 169 * 170 * import org.abego.*; 171 * 172 * import java.util.Set; 173 * 174 * public class SomeClass { ... } 175 * } 176 * </pre> 177 * 178 * 179 */ 180@FileStatefulCheck 181public class ImportOrderCheck 182 extends AbstractCheck { 183 184 /** 185 * A key is pointing to the warning message text in "messages.properties" 186 * file. 187 */ 188 public static final String MSG_SEPARATION = "import.separation"; 189 190 /** 191 * A key is pointing to the warning message text in "messages.properties" 192 * file. 193 */ 194 public static final String MSG_ORDERING = "import.ordering"; 195 196 /** 197 * A key is pointing to the warning message text in "messages.properties" 198 * file. 199 */ 200 public static final String MSG_SEPARATED_IN_GROUP = "import.groups.separated.internally"; 201 202 /** The special wildcard that catches all remaining groups. */ 203 private static final String WILDCARD_GROUP_NAME = "*"; 204 205 /** Empty array of pattern type needed to initialize check. */ 206 private static final Pattern[] EMPTY_PATTERN_ARRAY = new Pattern[0]; 207 208 /** List of import groups specified by the user. */ 209 private Pattern[] groups = EMPTY_PATTERN_ARRAY; 210 /** Require imports in group be separated. */ 211 private boolean separated; 212 /** Require imports in group. */ 213 private boolean ordered = true; 214 /** Should comparison be case sensitive. */ 215 private boolean caseSensitive = true; 216 217 /** Last imported group. */ 218 private int lastGroup; 219 /** Line number of last import. */ 220 private int lastImportLine; 221 /** Name of last import. */ 222 private String lastImport; 223 /** If last import was static. */ 224 private boolean lastImportStatic; 225 /** Whether there was any imports. */ 226 private boolean beforeFirstImport; 227 /** Whether static imports should be sorted alphabetically or not. */ 228 private boolean sortStaticImportsAlphabetically; 229 /** Whether to use container ordering (Eclipse IDE term) for static imports or not. */ 230 private boolean useContainerOrderingForStatic; 231 232 /** The policy to enforce. */ 233 private ImportOrderOption option = ImportOrderOption.UNDER; 234 235 /** 236 * Set the option to enforce. 237 * @param optionStr string to decode option from 238 * @throws IllegalArgumentException if unable to decode 239 */ 240 public void setOption(String optionStr) { 241 try { 242 option = ImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH)); 243 } 244 catch (IllegalArgumentException iae) { 245 throw new IllegalArgumentException("unable to parse " + optionStr, iae); 246 } 247 } 248 249 /** 250 * Sets the list of package groups and the order they should occur in the 251 * file. 252 * 253 * @param packageGroups a comma-separated list of package names/prefixes. 254 */ 255 public void setGroups(String... packageGroups) { 256 groups = new Pattern[packageGroups.length]; 257 258 for (int i = 0; i < packageGroups.length; i++) { 259 String pkg = packageGroups[i]; 260 final Pattern grp; 261 262 // if the pkg name is the wildcard, make it match zero chars 263 // from any name, so it will always be used as last resort. 264 if (WILDCARD_GROUP_NAME.equals(pkg)) { 265 // matches any package 266 grp = Pattern.compile(""); 267 } 268 else if (CommonUtil.startsWithChar(pkg, '/')) { 269 if (!CommonUtil.endsWithChar(pkg, '/')) { 270 throw new IllegalArgumentException("Invalid group"); 271 } 272 pkg = pkg.substring(1, pkg.length() - 1); 273 grp = Pattern.compile(pkg); 274 } 275 else { 276 final StringBuilder pkgBuilder = new StringBuilder(pkg); 277 if (!CommonUtil.endsWithChar(pkg, '.')) { 278 pkgBuilder.append('.'); 279 } 280 grp = Pattern.compile("^" + Pattern.quote(pkgBuilder.toString())); 281 } 282 283 groups[i] = grp; 284 } 285 } 286 287 /** 288 * Sets whether or not imports should be ordered within any one group of 289 * imports. 290 * 291 * @param ordered 292 * whether lexicographic ordering of imports within a group 293 * required or not. 294 */ 295 public void setOrdered(boolean ordered) { 296 this.ordered = ordered; 297 } 298 299 /** 300 * Sets whether or not groups of imports must be separated from one another 301 * by at least one blank line. 302 * 303 * @param separated 304 * whether groups should be separated by one blank line. 305 */ 306 public void setSeparated(boolean separated) { 307 this.separated = separated; 308 } 309 310 /** 311 * Sets whether string comparison should be case sensitive or not. 312 * 313 * @param caseSensitive 314 * whether string comparison should be case sensitive. 315 */ 316 public void setCaseSensitive(boolean caseSensitive) { 317 this.caseSensitive = caseSensitive; 318 } 319 320 /** 321 * Sets whether static imports (when grouped using 'top' and 'bottom' option) 322 * are sorted alphabetically or according to the package groupings. 323 * @param sortAlphabetically true or false. 324 */ 325 public void setSortStaticImportsAlphabetically(boolean sortAlphabetically) { 326 sortStaticImportsAlphabetically = sortAlphabetically; 327 } 328 329 /** 330 * Sets whether to use container ordering (Eclipse IDE term) for static imports or not. 331 * @param useContainerOrdering whether to use container ordering for static imports or not. 332 */ 333 public void setUseContainerOrderingForStatic(boolean useContainerOrdering) { 334 useContainerOrderingForStatic = useContainerOrdering; 335 } 336 337 @Override 338 public int[] getDefaultTokens() { 339 return getAcceptableTokens(); 340 } 341 342 @Override 343 public int[] getAcceptableTokens() { 344 return new int[] {TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT}; 345 } 346 347 @Override 348 public int[] getRequiredTokens() { 349 return new int[] {TokenTypes.IMPORT}; 350 } 351 352 @Override 353 public void beginTree(DetailAST rootAST) { 354 lastGroup = Integer.MIN_VALUE; 355 lastImportLine = Integer.MIN_VALUE; 356 lastImport = ""; 357 lastImportStatic = false; 358 beforeFirstImport = true; 359 } 360 361 // -@cs[CyclomaticComplexity] SWITCH was transformed into IF-ELSE. 362 @Override 363 public void visitToken(DetailAST ast) { 364 final int line = ast.getLineNo(); 365 final FullIdent ident; 366 final boolean isStatic; 367 368 if (ast.getType() == TokenTypes.IMPORT) { 369 ident = FullIdent.createFullIdentBelow(ast); 370 isStatic = false; 371 } 372 else { 373 ident = FullIdent.createFullIdent(ast.getFirstChild() 374 .getNextSibling()); 375 isStatic = true; 376 } 377 378 final boolean isStaticAndNotLastImport = isStatic && !lastImportStatic; 379 final boolean isLastImportAndNonStatic = lastImportStatic && !isStatic; 380 381 // using set of IF instead of SWITCH to analyze Enum options to satisfy coverage. 382 // https://github.com/checkstyle/checkstyle/issues/1387 383 if (option == ImportOrderOption.TOP) { 384 if (isLastImportAndNonStatic) { 385 lastGroup = Integer.MIN_VALUE; 386 lastImport = ""; 387 } 388 doVisitToken(ident, isStatic, isStaticAndNotLastImport, line); 389 390 if (isStaticAndNotLastImport && !beforeFirstImport) { 391 log(ident.getLineNo(), MSG_ORDERING, ident.getText()); 392 } 393 } 394 else if (option == ImportOrderOption.BOTTOM) { 395 if (isStaticAndNotLastImport) { 396 lastGroup = Integer.MIN_VALUE; 397 lastImport = ""; 398 } 399 doVisitToken(ident, isStatic, isLastImportAndNonStatic, line); 400 401 if (isLastImportAndNonStatic) { 402 log(ident.getLineNo(), MSG_ORDERING, ident.getText()); 403 } 404 } 405 else if (option == ImportOrderOption.ABOVE) { 406 // previous non-static but current is static 407 doVisitToken(ident, isStatic, isStaticAndNotLastImport, line); 408 } 409 else if (option == ImportOrderOption.UNDER) { 410 doVisitToken(ident, isStatic, isLastImportAndNonStatic, line); 411 } 412 else if (option == ImportOrderOption.INFLOW) { 413 // "previous" argument is useless here 414 doVisitToken(ident, isStatic, true, line); 415 } 416 else { 417 throw new IllegalStateException( 418 "Unexpected option for static imports: " + option); 419 } 420 421 lastImportLine = ast.findFirstToken(TokenTypes.SEMI).getLineNo(); 422 lastImportStatic = isStatic; 423 beforeFirstImport = false; 424 } 425 426 /** 427 * Shares processing... 428 * 429 * @param ident the import to process. 430 * @param isStatic whether the token is static or not. 431 * @param previous previous non-static but current is static (above), or 432 * previous static but current is non-static (under). 433 * @param line the line of the current import. 434 */ 435 private void doVisitToken(FullIdent ident, boolean isStatic, boolean previous, int line) { 436 final String name = ident.getText(); 437 final int groupIdx = getGroupNumber(name); 438 439 if (groupIdx > lastGroup) { 440 if (!beforeFirstImport && separated && line - lastImportLine < 2 441 && !isInSameGroup(groupIdx, isStatic)) { 442 log(line, MSG_SEPARATION, name); 443 } 444 } 445 else if (isInSameGroup(groupIdx, isStatic)) { 446 doVisitTokenInSameGroup(isStatic, previous, name, line); 447 } 448 else { 449 log(line, MSG_ORDERING, name); 450 } 451 if (isSeparatorInGroup(groupIdx, isStatic, line)) { 452 log(line, MSG_SEPARATED_IN_GROUP, name); 453 } 454 455 lastGroup = groupIdx; 456 lastImport = name; 457 } 458 459 /** 460 * Checks whether imports group separated internally. 461 * @param groupIdx group number. 462 * @param isStatic whether the token is static or not. 463 * @param line the line of the current import. 464 * @return true if imports group are separated internally. 465 */ 466 private boolean isSeparatorInGroup(int groupIdx, boolean isStatic, int line) { 467 final boolean inSameGroup = isInSameGroup(groupIdx, isStatic); 468 return (!separated || inSameGroup) && isSeparatorBeforeImport(line); 469 } 470 471 /** 472 * Checks whether there is any separator before current import. 473 * @param line the line of the current import. 474 * @return true if there is separator before current import which isn't the first import. 475 */ 476 private boolean isSeparatorBeforeImport(int line) { 477 return !beforeFirstImport && line - lastImportLine > 1; 478 } 479 480 /** 481 * Checks whether imports are in same group. 482 * @param groupIdx group number. 483 * @param isStatic whether the token is static or not. 484 * @return true if imports are in same group. 485 */ 486 private boolean isInSameGroup(int groupIdx, boolean isStatic) { 487 final boolean isStaticImportGroupIndependent = 488 option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM; 489 final boolean result; 490 if (isStaticImportGroupIndependent) { 491 result = isStatic && lastImportStatic 492 || groupIdx == lastGroup && isStatic == lastImportStatic; 493 } 494 else { 495 result = groupIdx == lastGroup; 496 } 497 return result; 498 } 499 500 /** 501 * Shares processing... 502 * 503 * @param isStatic whether the token is static or not. 504 * @param previous previous non-static but current is static (above), or 505 * previous static but current is non-static (under). 506 * @param name the name of the current import. 507 * @param line the line of the current import. 508 */ 509 private void doVisitTokenInSameGroup(boolean isStatic, 510 boolean previous, String name, int line) { 511 if (ordered) { 512 if (option == ImportOrderOption.INFLOW) { 513 if (isWrongOrder(name, isStatic)) { 514 log(line, MSG_ORDERING, name); 515 } 516 } 517 else { 518 final boolean shouldFireError = 519 // previous non-static but current is static (above) 520 // or 521 // previous static but current is non-static (under) 522 previous 523 || 524 // current and previous static or current and 525 // previous non-static 526 lastImportStatic == isStatic 527 && isWrongOrder(name, isStatic); 528 529 if (shouldFireError) { 530 log(line, MSG_ORDERING, name); 531 } 532 } 533 } 534 } 535 536 /** 537 * Checks whether import name is in wrong order. 538 * @param name import name. 539 * @param isStatic whether it is a static import name. 540 * @return true if import name is in wrong order. 541 */ 542 private boolean isWrongOrder(String name, boolean isStatic) { 543 final boolean result; 544 if (isStatic) { 545 if (useContainerOrderingForStatic) { 546 result = compareContainerOrder(lastImport, name, caseSensitive) >= 0; 547 } 548 else if (option == ImportOrderOption.TOP 549 || option == ImportOrderOption.BOTTOM) { 550 result = sortStaticImportsAlphabetically 551 && compare(lastImport, name, caseSensitive) >= 0; 552 } 553 else { 554 result = compare(lastImport, name, caseSensitive) >= 0; 555 } 556 } 557 else { 558 // out of lexicographic order 559 result = compare(lastImport, name, caseSensitive) >= 0; 560 } 561 return result; 562 } 563 564 /** 565 * Compares two import strings. 566 * We first compare the container of the static import, container being the type enclosing 567 * the static element being imported. When this returns 0, we compare the qualified 568 * import name. For e.g. this is what is considered to be container names: 569 * <p> 570 * import static HttpConstants.COLON => HttpConstants 571 * import static HttpHeaders.addHeader => HttpHeaders 572 * import static HttpHeaders.setHeader => HttpHeaders 573 * import static HttpHeaders.Names.DATE => HttpHeaders.Names 574 * </p> 575 * <p> 576 * According to this logic, HttpHeaders.Names would come after HttpHeaders. 577 * 578 * For more details, see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=473629#c3"> 579 * static imports comparison method</a> in Eclipse. 580 * </p> 581 * 582 * @param importName1 first import name. 583 * @param importName2 second import name. 584 * @param caseSensitive whether the comparison of fully qualified import names is case 585 * sensitive. 586 * @return the value {@code 0} if str1 is equal to str2; a value 587 * less than {@code 0} if str is less than the str2 (container order 588 * or lexicographical); and a value greater than {@code 0} if str1 is greater than str2 589 * (container order or lexicographically). 590 */ 591 private static int compareContainerOrder(String importName1, String importName2, 592 boolean caseSensitive) { 593 final String container1 = getImportContainer(importName1); 594 final String container2 = getImportContainer(importName2); 595 final int compareContainersOrderResult; 596 if (caseSensitive) { 597 compareContainersOrderResult = container1.compareTo(container2); 598 } 599 else { 600 compareContainersOrderResult = container1.compareToIgnoreCase(container2); 601 } 602 final int result; 603 if (compareContainersOrderResult == 0) { 604 result = compare(importName1, importName2, caseSensitive); 605 } 606 else { 607 result = compareContainersOrderResult; 608 } 609 return result; 610 } 611 612 /** 613 * Extracts import container name from fully qualified import name. 614 * An import container name is the type which encloses the static element being imported. 615 * For example, HttpConstants, HttpHeaders, HttpHeaders.Names are import container names: 616 * <p> 617 * import static HttpConstants.COLON => HttpConstants 618 * import static HttpHeaders.addHeader => HttpHeaders 619 * import static HttpHeaders.setHeader => HttpHeaders 620 * import static HttpHeaders.Names.DATE => HttpHeaders.Names 621 * </p> 622 * @param qualifiedImportName fully qualified import name. 623 * @return import container name. 624 */ 625 private static String getImportContainer(String qualifiedImportName) { 626 final int lastDotIndex = qualifiedImportName.lastIndexOf('.'); 627 return qualifiedImportName.substring(0, lastDotIndex); 628 } 629 630 /** 631 * Finds out what group the specified import belongs to. 632 * 633 * @param name the import name to find. 634 * @return group number for given import name. 635 */ 636 private int getGroupNumber(String name) { 637 int bestIndex = groups.length; 638 int bestEnd = -1; 639 int bestPos = Integer.MAX_VALUE; 640 641 // find out what group this belongs in 642 // loop over groups and get index 643 for (int i = 0; i < groups.length; i++) { 644 final Matcher matcher = groups[i].matcher(name); 645 if (matcher.find()) { 646 if (matcher.start() < bestPos) { 647 bestIndex = i; 648 bestEnd = matcher.end(); 649 bestPos = matcher.start(); 650 } 651 else if (matcher.start() == bestPos && matcher.end() > bestEnd) { 652 bestIndex = i; 653 bestEnd = matcher.end(); 654 } 655 } 656 } 657 658 return bestIndex; 659 } 660 661 /** 662 * Compares two strings. 663 * 664 * @param string1 665 * the first string. 666 * @param string2 667 * the second string. 668 * @param caseSensitive 669 * whether the comparison is case sensitive. 670 * @return the value {@code 0} if string1 is equal to string2; a value 671 * less than {@code 0} if string1 is lexicographically less 672 * than the string2; and a value greater than {@code 0} if 673 * string1 is lexicographically greater than string2. 674 */ 675 private static int compare(String string1, String string2, 676 boolean caseSensitive) { 677 final int result; 678 if (caseSensitive) { 679 result = string1.compareTo(string2); 680 } 681 else { 682 result = string1.compareToIgnoreCase(string2); 683 } 684 685 return result; 686 } 687 688}