View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2012 Oliver Burn
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.github.sevntu.checkstyle.checks.coding;
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.TreeMap;
29  
30  import com.github.sevntu.checkstyle.Utils;
31  import com.puppycrawl.tools.checkstyle.api.Check;
32  import com.puppycrawl.tools.checkstyle.api.DetailAST;
33  import com.puppycrawl.tools.checkstyle.api.FullIdent;
34  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35  
36  /**
37   * <p>
38   * This check can help you to write the whole for-each map iteration more
39   * correctly:
40   * </p>
41   * <p>
42   * 1. If you iterate over a map using map.keySet() or map.entrySet(), but your
43   * code uses only map values, Check will propose you to use map.values() instead
44   * of map.keySet() or map.entrySet(). Replacing map.keySet() or map.entrySet()
45   * with map.values() for such cases can a bit improve an iteration performance.
46   * <p>
47   * Bad:
48   * </p>
49   *
50   * <pre>
51   * for (Map.Entry<String, String>; entry : map.entrySet())
52   * {
53   *     System.out.println(entry.getValue());
54   * }
55   * </pre>
56   *
57   * <pre>
58   * for (String key : map.keySet())
59   * {
60   *     System.out.println(map.get(key));
61   * }
62   * </pre>
63   * <p>
64   * Good:
65   * </p>
66   *
67   * <pre>
68   * for (String value : map.values())
69   * {
70   *     System.out.println(value);
71   * }
72   * </pre>
73   *
74   * 2. If you iterate over a map using map.entrySet(), but never call
75   * entry.getValue(), Check will propose you to use map.keySet() instead of
76   * map.entrySet(). to iterate over map keys only.
77   * <p>
78   * Bad:
79   * </p>
80   *
81   * <pre>
82   * for (Map.Entry<String, String> entry : map.entrySet())
83   * {
84   *     System.out.println(entry.getKey());
85   * }
86   * </pre>
87   * <p>
88   * Good:
89   * </p>
90   *
91   * <pre>
92   * for (String key : map.keySet())
93   * {
94   *     System.out.println(key);
95   * }
96   * </pre>
97   *
98   * 3. If you iterate over a map with map.keySet() and use both keys and values,
99   * check will propose you to use map.entrySet() to improve an iteration
100  * performance by avoiding search operations inside a map. For this case,
101  * iteration can significantly grow up a performance.
102  * <p>
103  * Bad:
104  * </p>
105  *
106  * <pre>
107  * for (String key : map.keySet())
108  * {
109  *     System.out.println(key + "  " + map.get(key));
110  * }
111  * </pre>
112  * <p>
113  * Good:
114  * </p>
115  *
116  * <pre>
117  * for (Map.Entry<String, String> entry : map.entrySet())
118  * {
119  *     System.out.println(entry.getValue() + "   " + entry.getKey());
120  * }
121  * </pre>
122  * @author <a href="mailto:maxvetrenko2241@gmail.com">Max Vetrenko</a>
123  */
124 
125 public class MapIterationInForEachLoopCheck extends Check
126 {
127     /**
128      * If this value is true, Checkstyle will process value() iterations.
129      */
130     private boolean proposeValuesUsage = true;
131 
132     /**
133      * If this value is true, Checkstyle will process keySet() iterations.
134      */
135     private boolean proposeKeySetUsage = false;
136 
137     /**
138      * If this value is true, Checkstyle will process entrySet() iterations.
139      */
140     private boolean proposeEntrySetUsage = false;
141 
142     /**
143      * The key is pointing to the warning message text in "messages.properties"
144      * file.
145      */
146     public static final String MSG_KEY_KEYSET = "map.iteration.keySet";
147 
148     /**
149      * The key is pointing to the warning message text in "messages.properties"
150      * file.
151      */
152     public static final String MSG_KEY_ENTRYSET = "map.iteration.entrySet";
153 
154     /**
155      * The key is pointing to the warning message text in "messages.properties"
156      * file.
157      */
158     public static final String MSG_KEY_VALUES = "map.iteration.values";
159 
160     /**
161      * The name of keySet() method.
162      */
163     private static final String KEY_SET_METHOD_NAME = "keySet";
164 
165     /**
166      * The name of entrySet() method.
167      */
168     private static final String ENTRY_SET_METHOD_NAME = "entrySet";
169 
170     /**
171      * The name of get() method.
172      */
173     private static final String GET_NODE_NAME = "get";
174 
175     /**
176      * The name of getValue() method.
177      */
178     private static final String GET_VALUE_NODE_NAME = "getValue";
179 
180     /**
181      * The name of getKey() method.
182      */
183     private static final String GET_KEY_NODE_NAME = "getKey";
184 
185     /**
186      * This list contains Map object's names.
187      */
188     private List<String> mapNamesList = new ArrayList<String>();
189 
190     /**
191      * This list contains all qualified imports.
192      */
193     private List<String> qualifiedImportList = new ArrayList<String>();
194 
195     /**
196      * Set of allowable map implementations. You can set your own map
197      * implementations in Checkstyle configuration
198      */
199     private final Set<String> supportedMapImplQualifiedNames = new HashSet<String>();
200 
201     /**
202      * Creates default importList and mapImportClassesNamesList.
203      */
204     public MapIterationInForEachLoopCheck()
205     {
206         setSupportedMapImplQualifiedNames(new String[] {
207                                                         Map.class.getName(), TreeMap.class.getName(),
208                                                         HashMap.class.getName(), });
209     }
210 
211     /**
212      * Set user's map implementations. It must state the full paths of imported
213      * classes. Import paths must be separated by commas. For example:
214      * java.util.Map, java.util.HashMap.
215      * @param setSupportedMapImplQualifiedNames
216      *        User's set of map implementations.
217      */
218     public void setSupportedMapImplQualifiedNames(
219             final String[] setSupportedMapImplQualifiedNames)
220     {
221         supportedMapImplQualifiedNames.clear();
222         if (setSupportedMapImplQualifiedNames != null) {
223             for (String name : setSupportedMapImplQualifiedNames) {
224                 supportedMapImplQualifiedNames.add(name);
225                 final String importPathWithoutClassName = name.substring(0,
226                         name.lastIndexOf(".") + 1) + "*";
227                 supportedMapImplQualifiedNames.add(importPathWithoutClassName);
228             }
229         }
230     }
231 
232     /**
233      * Set aProcessingValue. If value is true, Check will process cases, where
234      * values() method will be suitable.
235      * @param proposeValuesUsage
236      *        User's value of mProcessingValue.
237      */
238     public void setProposeValuesUsage(
239             final boolean proposeValuesUsage)
240     {
241         this.proposeValuesUsage = proposeValuesUsage;
242     }
243 
244     /**
245      * Set aProcessingKeySet. If value is true, Check will process cases, where
246      * keySet() method will be suitable.
247      * @param proposeKeySetUsage
248      *        User's value of mIsCheckKeySetProcessingEnabled.
249      */
250     public void setProposeKeySetUsage(
251             final boolean proposeKeySetUsage)
252     {
253         this.proposeKeySetUsage = proposeKeySetUsage;
254     }
255 
256     /**
257      * Set aProcessingEntrySet. If value is true, Check will process cases,
258      * where entrySet() method will be suitable.
259      * @param proposeEntrySetUsage
260      *        User's value of mIsCheckEntrySetProcessingEnabled.
261      */
262     public void setProposeEntrySetUsage(
263             final boolean proposeEntrySetUsage)
264     {
265         this.proposeEntrySetUsage = proposeEntrySetUsage;
266     }
267 
268     @Override
269     public int[] getDefaultTokens()
270     {
271         return new int[] {TokenTypes.LITERAL_FOR, TokenTypes.IMPORT, TokenTypes.VARIABLE_DEF, };
272     }
273 
274     @Override
275     public void beginTree(DetailAST ast)
276     {
277         qualifiedImportList.clear();
278         mapNamesList.clear();
279     }
280 
281     @Override
282     public void visitToken(DetailAST ast)
283     {
284         switch (ast.getType()) {
285 
286             case TokenTypes.IMPORT:
287                 String qualifiedMapImportText = getMapImportQualifiedName(ast);
288                 if (qualifiedMapImportText != null) {
289                     qualifiedImportList.add(qualifiedMapImportText);
290                 }
291                 break;
292 
293             case TokenTypes.VARIABLE_DEF:
294                 if (!qualifiedImportList.isEmpty() && isMapVariable(ast)) {
295                     DetailAST mapIdentNode = ast.findFirstToken(TokenTypes.TYPE).getNextSibling();
296                     String mapName = mapIdentNode.getText();
297                     //If Map name is contains into mMapNamesList, it doesn't need second inclusion
298                     if (!mapNamesList.contains(mapName)) {
299                         mapNamesList.add(mapIdentNode.getText());
300                     }
301                 }
302                 break;
303 
304             case TokenTypes.LITERAL_FOR:
305                 if (!qualifiedImportList.isEmpty() && isForEach(ast)) {
306                     final String warningMessageKey = validate(ast);
307                     if (warningMessageKey != null) {
308                         log(ast, warningMessageKey);
309                     }
310                 }
311                 break;
312 
313             default:
314                 Utils.reportInvalidToken(ast.getType());
315                 break;
316         }
317     }
318 
319     /**
320      * Processes "for-each" loop.
321      * It searches for keySet() or entrySet() nodes,
322      * iterated maps, keys or entries.
323      * @param forLiteralNode
324      *        DetailAST of literal for.
325      * @return warning message key.
326      */
327     private String validate(DetailAST forLiteralNode)
328     {
329         String warningMessageKey = null;
330         final DetailAST forEachNode = forLiteralNode.findFirstToken(TokenTypes.FOR_EACH_CLAUSE);
331         final DetailAST keySetOrEntrySetNode =
332                 getKeySetOrEntrySetNode(forEachNode);
333         boolean isMapClassField = false;
334         // Search for keySet or entrySet
335         if (keySetOrEntrySetNode != null) {
336             if (keySetOrEntrySetNode.getPreviousSibling().getChildCount() != 0) {
337                 isMapClassField = true;
338             }
339             final DetailAST variableDefNode = forEachNode.getFirstChild();
340             final String keyOrEntryVariableName = variableDefNode.getLastChild().getText();
341 
342             final String currentMapVariableName = isMapClassField ?
343                     keySetOrEntrySetNode.getPreviousSibling().getLastChild().getText()
344                     :keySetOrEntrySetNode.getPreviousSibling().getText();
345             final DetailAST forEachOpeningBrace = forLiteralNode.getLastChild();
346 
347             if (!isMapPassedIntoAnyMethod(forEachOpeningBrace)) {
348 
349                 if (proposeKeySetUsage
350                         && KEY_SET_METHOD_NAME.equals(
351                                 keySetOrEntrySetNode.getText()))
352                 {
353                     warningMessageKey =
354                             checkForWrongKeySetUsage(forEachOpeningBrace,
355                             keyOrEntryVariableName, currentMapVariableName, isMapClassField);
356                 }
357                 else if (proposeEntrySetUsage) {
358                     warningMessageKey = checkForWrongEntrySetUsage(forEachOpeningBrace,
359                             keyOrEntryVariableName);
360                 }
361             }
362         }
363         return warningMessageKey;
364     }
365 
366     /**
367      * 
368      * @param forNode
369      * @return
370      */
371     private static boolean isForEach(DetailAST forNode)
372     {
373         return forNode.findFirstToken(TokenTypes.FOR_EACH_CLAUSE) != null;
374     }
375     
376     /**
377      * Searches for keySet() or entrySet() node.
378      * @param forEachNode
379      *        Contains current for node.
380      * @return keySet() or entrySet() node. If such node didn't found, method
381      *         return null.
382      */
383     private DetailAST getKeySetOrEntrySetNode(DetailAST forEachNode)
384     {
385         final List<DetailAST> identAndThisNodesList = getSubTreeNodesOfType(forEachNode,
386                 TokenTypes.IDENT, TokenTypes.LITERAL_THIS);
387         boolean isMapClassField = false;
388         for (DetailAST thisNode : identAndThisNodesList) {
389             if(thisNode.getType() == TokenTypes.LITERAL_THIS) {
390                 isMapClassField = true;
391                 break;
392             }
393         }
394         DetailAST keySetOrEntrySetNode = null;
395         for (DetailAST identNode : identAndThisNodesList) {
396             if (KEY_SET_METHOD_NAME.equals(identNode.getText())
397                     || ENTRY_SET_METHOD_NAME.equals(identNode.getText()))
398             {
399                 String mapClassName = isMapClassField
400                         ? identNode.getPreviousSibling().getLastChild().getText()
401                                 : identNode.getPreviousSibling().getText();
402                 if (mapNamesList.contains(mapClassName)) {
403                     keySetOrEntrySetNode = identNode;
404                     break;
405                 }
406             }
407         }
408         return keySetOrEntrySetNode;
409     }
410 
411     /**
412      * Returns true, if any method call inside for loop contains map
413      * object as parameter.
414      * @param forEachOpeningBraceNode
415      *        List with subtree IDENT nodes.
416      * @return true, if any Method Call contains Map Parameter.
417      */
418     private boolean isMapPassedIntoAnyMethod(DetailAST forEachOpeningBraceNode)
419     {
420         final List<DetailAST> methodCallNodeList = getSubTreeNodesOfType(
421                 forEachOpeningBraceNode, TokenTypes.METHOD_CALL);
422         for (DetailAST methodCallNode : methodCallNodeList) {
423             if (hasMapAsParameter(methodCallNode)) {
424                 return true;
425             }
426         }
427         return false;
428     }
429 
430     /**
431      * Checks is map instance passed into method call, or not.
432      * @param methodCallNode
433      *        DetailAST node of Method Call.
434      * @return return true, if method call contain map as parameter.
435      */
436     private boolean hasMapAsParameter(DetailAST methodCallNode)
437     {
438         boolean result = false;
439         final List<DetailAST> identNodesList = getSubTreeNodesOfType(methodCallNode,
440                 TokenTypes.IDENT);
441         for (String mapName : mapNamesList) {
442             for (DetailAST identNode : identNodesList) {
443                 if (mapName.equals(identNode.getText())
444                         && identNode.getParent().getType() == TokenTypes.EXPR)
445                 {
446                     result =  true;
447                 }
448             }
449         }
450         return result;
451     }
452 
453     /**
454      * Searches for wrong ketSet() usage into for cycles.
455      * @param forEachOpeningBraceNode
456      *        For-each opening brace.
457      * @param keyName
458      *        Map's key name.
459      * @param mapName
460      *        Current map name.
461      * @return keySet warning message key.
462      */
463     private String
464     checkForWrongKeySetUsage(DetailAST forEachOpeningBraceNode,
465             String keyName, String mapName, boolean isMapClassField)
466     {
467         String result = null;
468 
469         final List<DetailAST> identAndLiteralIfNodesList =
470                 getSubTreeNodesOfType(forEachOpeningBraceNode,
471                         TokenTypes.IDENT, TokenTypes.LITERAL_IF);
472         int methodGetCallCount = 0;
473         int keyIdentCount = 0;
474         for (DetailAST identOrLiteralIfNode : identAndLiteralIfNodesList) {
475             DetailAST mapIdentNode = identOrLiteralIfNode.getPreviousSibling();
476             if (isMapClassField && mapIdentNode != null) {
477                 mapIdentNode = mapIdentNode.getLastChild();
478             }
479             if (mapIdentNode != null && GET_NODE_NAME.equals(identOrLiteralIfNode.getText())
480                     && mapName.equals(mapIdentNode.getText()))
481             {        
482 
483                 methodGetCallCount++;
484             }
485 
486             if (keyName.equals(identOrLiteralIfNode.getText())) {
487                 keyIdentCount++;
488             }
489         }
490 
491         final DetailAST literalIfNode =
492                 getFirstNodeOfType(identAndLiteralIfNodesList,
493                         TokenTypes.LITERAL_IF);
494         int methodGetCallInsideIfCount = 0;
495         if (literalIfNode != null) {
496             for (DetailAST node : getSubTreeNodesOfType(literalIfNode, TokenTypes.IDENT))
497             {
498                 DetailAST mapIdentNode = node.getPreviousSibling();
499                 if (isMapClassField && mapIdentNode != null) {
500                     mapIdentNode = mapIdentNode.getLastChild();
501                 }
502 
503                 if (mapIdentNode != null && GET_NODE_NAME.equals(node.getText())
504                         && mapName.equals(mapIdentNode.getText()))
505                 {
506                     methodGetCallInsideIfCount++;
507                 }
508             }
509         }
510 
511         if (methodGetCallCount != 0 && keyIdentCount != 0) {
512 
513             if (proposeValuesUsage && methodGetCallCount == keyIdentCount) {
514                 result = MSG_KEY_VALUES;
515             }
516 
517             else if (methodGetCallCount < keyIdentCount && methodGetCallCount > 0
518                     && methodGetCallInsideIfCount != methodGetCallCount)
519             {
520                 result = MSG_KEY_ENTRYSET;
521             }
522         }
523         return result;
524     }
525 
526     /**
527      * Searches for wrong entrySet() usage inside for cycles.
528      * @param forEachOpeningBraceNode
529      *        For-each opening brace.
530      * @param entryName
531      *        This variable contains Map.Entry name.
532      * @return entrySet warning message key.
533      */
534     private String
535     checkForWrongEntrySetUsage(DetailAST forEachOpeningBraceNode, String entryName)
536     {
537         String result = null;
538 
539         final List<DetailAST> identNodesList = getSubTreeNodesOfType(
540                 forEachOpeningBraceNode, TokenTypes.IDENT);
541         int methodGetKeyCallCount = 0;
542         int methodGetValueCallCount = 0;
543         for (DetailAST identNode : identNodesList) {
544 
545             final DetailAST entryNode = identNode.getPreviousSibling();
546 
547             if (entryNode != null && GET_KEY_NODE_NAME.equals(identNode.getText()) 
548                     && entryName.equals(entryNode.getText()))
549             {
550                 methodGetKeyCallCount++;
551             }
552 
553             if (entryNode != null && GET_VALUE_NODE_NAME.equals(identNode.getText())
554                     && entryName.equals(entryNode.getText()))
555             {
556                 methodGetValueCallCount++;
557             }
558         }
559 
560         if (proposeValuesUsage
561                 && methodGetKeyCallCount == 0 && methodGetValueCallCount > 0)
562         {
563             result = MSG_KEY_VALUES;
564         }
565         else if (methodGetKeyCallCount > 0 && methodGetValueCallCount == 0) {
566             result = MSG_KEY_KEYSET;
567 
568         }
569         return result;
570     }
571 
572     /**
573      * Checks if the new variable is Map object, or not.
574      * @param variableDefNode
575      *        DetailAST node of Variable Definition.
576      * @return true, if the new variable is Map object.
577      */
578     private boolean isMapVariable(DetailAST variableDefNode)
579     {
580         boolean result = false;
581         final List<DetailAST> literaNewNodeslList =
582                 getSubTreeNodesOfType(variableDefNode,
583                         TokenTypes.LITERAL_NEW, TokenTypes.ASSIGN);
584         final String className = getClassName(literaNewNodeslList);
585         if (getFirstNodeOfType(literaNewNodeslList, TokenTypes.ASSIGN)
586                 != null && className != null) {
587             result = isMapImplementation(className);
588         }
589         return result;
590     }
591 
592     /**
593      * Checks, is current class a Map implementation or not.
594      * @param className
595      *        Current class's name.
596      * @return true, if current class is contained inside mQualifiedImportList.
597      */
598     private boolean isMapImplementation(String className)
599     {
600         return isClassContainsInsideQualifiedImportList(className)
601                 || containsInSupportedMapImplQualifiedNames(className);
602     }
603 
604     /**
605      * Checks, is mSupportedMapImplQualifiedNames List contains
606      * current class.
607      * @param className
608      *        current class name.
609      * @return true, if List contains current class.
610      */
611     private boolean containsInSupportedMapImplQualifiedNames(String className)
612     {
613         boolean result = false;
614         for (String supportedMapName : supportedMapImplQualifiedNames) {
615             if (supportedMapName.endsWith(className)) {
616                 final int lastDotIndex = supportedMapName.lastIndexOf(".") + 1;
617                 final String packageName = supportedMapName.substring(0, lastDotIndex) + "*";
618                 if (qualifiedImportList.contains(packageName)) {
619                     result = true;
620                     break;
621                 }
622             }
623         }
624         return result;
625     }
626 
627     /**
628      * Checks, is mQualifiedImportList contains
629      * current class.
630      * @param className
631      *        current class name.
632      * @return true, if List contains current class.
633      */
634     private boolean isClassContainsInsideQualifiedImportList(String className)
635     {
636         boolean result = false;
637         for (String mapImplementationQualifiedName : qualifiedImportList) {
638             if (mapImplementationQualifiedName.endsWith(className)) {
639                 result = true;
640                 break;
641             }
642         }
643         return result;
644     }
645 
646     /**
647      * Returns the instance's class name.
648      * @param literaNewNodesList
649      *        This list contains "new" literals.
650      * @return object's class name,
651      *        if class name is missed, returns null.
652      */
653     private static String getClassName(final List<DetailAST> literaNewNodesList)
654     {
655         for (DetailAST literalNewNode : literaNewNodesList) {
656             DetailAST exprNode = literalNewNode.getParent();
657             if (exprNode.getParent().getType() == TokenTypes.ASSIGN) {
658                 return literalNewNode.getFirstChild().getText();
659             }
660         }
661         return null;
662     }
663 
664     /**
665      * Searches the first specific
666      * DetailAST node inside List.
667      * @param nodesList
668      *        DetailAST List witch maybe contains specific token.
669      * @param aSpecificType
670      *        A specific type of token.
671      * @return specific token or null.
672      */
673     private static DetailAST getFirstNodeOfType(List<DetailAST> nodesList,
674             int aSpecificType)
675     {
676         for (DetailAST node : nodesList) {
677             if (node.getType() == aSpecificType) {
678                 return node;
679             }
680         }
681         return null;
682     }
683 
684     /**
685      * Returns full path of map implementation. If path doesn't
686      * contain full map implementation path, null will be returned.
687      * @param importNode
688      *        Import node.
689      * @return full path of map implementation or null.
690      */
691     private String getMapImportQualifiedName(DetailAST importNode)
692     {
693         final String mapClassQualifiedName = FullIdent.createFullIdent(
694                 importNode.getFirstChild()).getText();
695         for (String qualifiedName : supportedMapImplQualifiedNames) {
696             if (mapClassQualifiedName.equals(qualifiedName)) {
697                 return mapClassQualifiedName;
698             }
699         }
700         return null;
701     }
702 
703     /**
704      * Searches over subtree for all tokens of necessary types.
705      * @param rootNode
706      *        The root of subtree.
707      * @param tokenTypes
708      *        Token's necessary types into If condition.
709      * @return DetailAST List with necessary tokens.
710      */
711     private static List<DetailAST> getSubTreeNodesOfType(DetailAST rootNode,
712             int... tokenTypes)
713     {
714         final List<DetailAST> result = new ArrayList<DetailAST>();
715         final DetailAST finishNode;
716         if (rootNode.getNextSibling() == null) {
717             finishNode = rootNode.getLastChild();
718         }
719         else {
720             finishNode = rootNode.getNextSibling();
721         }
722         DetailAST curNode = rootNode;
723         while (curNode != null && curNode != finishNode) {
724             for (int tokenType : tokenTypes) {
725                 if (curNode.getType() == tokenType) {
726                     result.add(curNode);
727                 }
728             }
729             DetailAST toVisit = curNode.getFirstChild();
730             while ((curNode != null) && (toVisit == null)) {
731                 toVisit = curNode.getNextSibling();
732                 if (toVisit == null) {
733                     curNode = curNode.getParent();
734                 }
735             }
736             curNode = toVisit;
737         }
738         return result;
739     }
740 }