Added
Link Here
|
1 |
/******************************************************************************* |
2 |
* Copyright (c) 2010 Rita Chow, Nicola Hall, Jerry Hsiao, Mark Mozolewski, Chamil Wijenayaka |
3 |
* All rights reserved. This program and the accompanying materials |
4 |
* are made available under the terms of the Eclipse Public License v1.0 |
5 |
* which accompanies this distribution, and is available at |
6 |
* http://www.eclipse.org/legal/epl-v10.html |
7 |
* |
8 |
* Contributors: |
9 |
* Rita Chow - Initial Implementation |
10 |
* Nicola Hall - Initial Implementation |
11 |
* Jerry Hsiao - Initial Implementation |
12 |
* Mark Mozolewski - Initial Implementation |
13 |
* Chamil Wijenayaka - Initial Implementation |
14 |
*******************************************************************************/ |
15 |
package org.eclipse.photran.internal.core.refactoring; |
16 |
|
17 |
import java.util.LinkedList; |
18 |
import org.eclipse.core.runtime.CoreException; |
19 |
import org.eclipse.core.runtime.IProgressMonitor; |
20 |
import org.eclipse.core.runtime.OperationCanceledException; |
21 |
import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
22 |
import org.eclipse.photran.internal.core.analysis.binding.ScopingNode; |
23 |
import org.eclipse.photran.internal.core.analysis.loops.ASTProperLoopConstructNode; |
24 |
import org.eclipse.photran.internal.core.analysis.loops.ASTVisitorWithLoops; |
25 |
import org.eclipse.photran.internal.core.analysis.loops.LoopReplacer; |
26 |
import org.eclipse.photran.internal.core.parser.*; |
27 |
import org.eclipse.photran.internal.core.refactoring.infrastructure.FortranEditorRefactoring; |
28 |
|
29 |
/** |
30 |
* Remove branching to END IF statement from outside its IF ... END IF block. Such branching should |
31 |
* be replaced with branching to CONTINUE statement that immediately follows the END IF statement. |
32 |
* If the END IF statement is followed by a CONTINUE statement then outside GOTO branches should |
33 |
* target the CONTINUE statement. If one does not exist the refactoring will insert one and target |
34 |
* it. GOTOs inside the selected IF block are not re-targeted. |
35 |
* |
36 |
* User Selection Requirements: The labeled END IF that they want considered for the refactoring. |
37 |
* |
38 |
* @author Rita Chow (chow15), Jerry Hsiao (jhsiao2), Mark Mozolewski (mozolews), Chamil Wijenayaka |
39 |
* (wijenay2), Nicola Hall (nfhall2) |
40 |
*/ |
41 |
public class RemoveBranchToEndIfRefactoring extends FortranEditorRefactoring |
42 |
{ |
43 |
private IASTNode selectedNode; |
44 |
|
45 |
private ASTEndIfStmtNode selectedEndIfNode; |
46 |
|
47 |
private ASTIfConstructNode selectedIfConstructNode; |
48 |
|
49 |
private LinkedList<ASTGotoStmtNode> gotoStatements; |
50 |
|
51 |
private LinkedList<ASTGotoStmtNode> selectedGotoStatements; |
52 |
|
53 |
private ASTContinueStmtNode continueAfterIfStmtNode; |
54 |
|
55 |
private LinkedList<ASTGotoStmtNode> selectedGotoStatementWithEndIfLabel; |
56 |
|
57 |
private LinkedList<ASTGotoStmtNode> gotoStatementWithEndIfLabel; |
58 |
|
59 |
/** |
60 |
* Provide GUI refactoring label. |
61 |
*/ |
62 |
@Override |
63 |
public String getName() |
64 |
{ |
65 |
return Messages.RemoveBranchToEndIfRefactoring_Name; |
66 |
} |
67 |
|
68 |
/** |
69 |
* Preconditions that are checked before the refactoring is applied are: 1) END IF selected must |
70 |
* be labeled and part of an IF block. 2) GOTO from either inside the selected IF block or |
71 |
* outside the selected IF block must target the label of the END IF selected. 3) must be more |
72 |
* GOTOs than just in local selected END IF scope. |
73 |
* |
74 |
*/ |
75 |
@Override |
76 |
protected void doCheckInitialConditions(RefactoringStatus status, IProgressMonitor pm) |
77 |
throws PreconditionFailure |
78 |
{ |
79 |
ensureProjectHasRefactoringEnabled(status); |
80 |
|
81 |
// Check if selected text is a portion within an END IF statement with label. |
82 |
setSelectedNodeAndEndIfNode(); |
83 |
if ((selectedEndIfNode == null) || (selectedEndIfNode.getLabel() == null)) |
84 |
{ |
85 |
System.out |
86 |
.println(Messages.RemoveBranchToEndIfRefactoring_SelectEndIfStatementToRefactor); |
87 |
fail(Messages.RemoveBranchToEndIfRefactoring_SelectEndIfStatementToRefactor); |
88 |
} |
89 |
|
90 |
// Check if selected END IF is within an IF block. |
91 |
setSelectedIfConstructNode(); |
92 |
if (selectedIfConstructNode == null) |
93 |
{ |
94 |
System.out.println(Messages.RemoveBranchToEndIfRefactoring_CanNotFindIfConstruct); |
95 |
fail(Messages.RemoveBranchToEndIfRefactoring_CanNotFindIfConstruct); |
96 |
} |
97 |
|
98 |
// Check if selected scope contains a branch. |
99 |
setGotoStatements(); |
100 |
if (gotoStatements.isEmpty()) |
101 |
{ |
102 |
System.out.println(Messages.RemoveBranchToEndIfRefactoring_MissingGoToStatement); |
103 |
fail(Messages.RemoveBranchToEndIfRefactoring_MissingGoToStatement); |
104 |
} |
105 |
|
106 |
// Check if contains a GOTO statement with selected END IF label. |
107 |
setGotoStatementsWithEndIfLabel(); |
108 |
if (gotoStatementWithEndIfLabel.size() == 0) |
109 |
{ |
110 |
System.out |
111 |
.println(Messages.RemoveBranchToEndIfRefactoring_CanNotFindGoToStatementForThisLabel); |
112 |
fail(Messages.RemoveBranchToEndIfRefactoring_CanNotFindGoToStatementForThisLabel); |
113 |
} |
114 |
|
115 |
// Check to see if there are branches outside of selected if block to selected END IF label. |
116 |
setSelectedGotoStatementsWithEndIfLabel(); |
117 |
if (selectedGotoStatementWithEndIfLabel.size() == gotoStatementWithEndIfLabel.size()) |
118 |
{ |
119 |
System.out.println(Messages.RemoveBranchToEndIfRefactoring_BranchToImmediateEndIf); |
120 |
fail(Messages.RemoveBranchToEndIfRefactoring_BranchToImmediateEndIf); |
121 |
} |
122 |
|
123 |
setContinueAfterIfStmtNode(); |
124 |
} |
125 |
|
126 |
/** |
127 |
* There are no final conditions. |
128 |
*/ |
129 |
@Override |
130 |
protected void doCheckFinalConditions(RefactoringStatus status, IProgressMonitor pm) |
131 |
throws PreconditionFailure |
132 |
{ |
133 |
// No final conditions |
134 |
} |
135 |
|
136 |
/** |
137 |
* Separate refactroing methods based on if there is a CONTINUE statement after the selected END |
138 |
* IF (if so target it) or if no CONTINUE statement exists after the END IF block (insert one |
139 |
* with unique label and have GOTO statements outside the IF block target it.). |
140 |
*/ |
141 |
@Override |
142 |
protected void doCreateChange(IProgressMonitor pm) throws CoreException, |
143 |
OperationCanceledException |
144 |
{ |
145 |
setAstAndNodes(); |
146 |
|
147 |
if (continueAfterIfStmtNode == null) |
148 |
{ |
149 |
changeNoContinueAfterEndIf(); // User Story #2 |
150 |
|
151 |
} |
152 |
else |
153 |
{ |
154 |
changeGotoLabelToContinueLabel(); // User Story #1 |
155 |
} |
156 |
|
157 |
// User Story 3 checked in doCheckInitialConditions |
158 |
|
159 |
this.addChangeFromModifiedAST(this.fileInEditor, pm); |
160 |
vpg.releaseAST(this.fileInEditor); |
161 |
} |
162 |
|
163 |
/** |
164 |
* Sets AST of selected file and nodes used for refactoring. |
165 |
* |
166 |
* @param None. |
167 |
* |
168 |
* @return None. |
169 |
*/ |
170 |
private void setAstAndNodes() |
171 |
{ |
172 |
this.astOfFileInEditor = vpg.acquireTransientAST(fileInEditor); |
173 |
|
174 |
// Setup nodes used for refactoring |
175 |
setSelectedNodeAndEndIfNode(); |
176 |
setSelectedIfConstructNode(); |
177 |
setGotoStatements(); |
178 |
setGotoStatementsWithEndIfLabel(); |
179 |
setSelectedGotoStatementsWithEndIfLabel(); |
180 |
setContinueAfterIfStmtNode(); |
181 |
} |
182 |
|
183 |
/** |
184 |
* Set selected node and selected end if node used for refactoring. |
185 |
* |
186 |
* @param None. |
187 |
* |
188 |
* @return None. |
189 |
*/ |
190 |
private void setSelectedNodeAndEndIfNode() |
191 |
{ |
192 |
selectedNode = findEnclosingNode(this.astOfFileInEditor, this.selectedRegionInEditor); |
193 |
selectedEndIfNode = (ASTEndIfStmtNode)findEnclosingNodeOfType(selectedNode, |
194 |
ASTEndIfStmtNode.class); |
195 |
} |
196 |
|
197 |
/** |
198 |
* Set selected if construct node used for refactoring. |
199 |
* |
200 |
* @param None. |
201 |
* |
202 |
* @return None. |
203 |
*/ |
204 |
private void setSelectedIfConstructNode() |
205 |
{ |
206 |
selectedIfConstructNode = (ASTIfConstructNode)findEnclosingNodeOfType(selectedEndIfNode, |
207 |
ASTIfConstructNode.class); |
208 |
} |
209 |
|
210 |
/** |
211 |
* Set goto statements used for refactoring. |
212 |
* |
213 |
* @param None. |
214 |
* |
215 |
* @return None. |
216 |
*/ |
217 |
private void setGotoStatements() |
218 |
{ |
219 |
gotoStatements = getGotoNodes(ScopingNode.getLocalScope(selectedEndIfNode)); |
220 |
} |
221 |
|
222 |
/** |
223 |
* Set go to statements with end if label used for refactoring. |
224 |
* |
225 |
* @param None. |
226 |
* |
227 |
* @return None. |
228 |
*/ |
229 |
private void setGotoStatementsWithEndIfLabel() |
230 |
{ |
231 |
gotoStatementWithEndIfLabel = findGotoForLabel(gotoStatements, selectedEndIfNode.getLabel() |
232 |
.getText()); |
233 |
} |
234 |
|
235 |
/** |
236 |
* Set selected go to statements with end if label used for refactoring. |
237 |
* |
238 |
* @param None. |
239 |
* |
240 |
* @return None. |
241 |
*/ |
242 |
private void setSelectedGotoStatementsWithEndIfLabel() |
243 |
{ |
244 |
selectedGotoStatements = getGotoNodes(selectedIfConstructNode); |
245 |
selectedGotoStatementWithEndIfLabel = findGotoForLabel(selectedGotoStatements, |
246 |
selectedEndIfNode.getLabel().getText()); |
247 |
} |
248 |
|
249 |
/** |
250 |
* Set continue after if statement used for refactoring. |
251 |
* |
252 |
* @param None. |
253 |
* |
254 |
* @return None. |
255 |
*/ |
256 |
private void setContinueAfterIfStmtNode() |
257 |
{ |
258 |
continueAfterIfStmtNode = continueAfterIfStmt(selectedIfConstructNode); |
259 |
} |
260 |
|
261 |
/** |
262 |
* Main refactoring code for condition when CONTINUE statement follows the END..IF that was |
263 |
* selected for refactoring. The logic will renumber/retarget any GOTO statements in the GOTO to |
264 |
* that of the CONINUE label. (Per FORTRAN language standard the CONTINUE statement must have a |
265 |
* label). If any GOTO statement inside the selected END..IF targets the END..IF selected for |
266 |
* the Refactoring then the label of the END..IF will not be removed and that inner GOTO will |
267 |
* still target it. If there are no inner GOTOs targeting the END..IF label then the END..IF |
268 |
* label will be removed as part of the refactoring. |
269 |
* |
270 |
*/ |
271 |
private void changeGotoLabelToContinueLabel() |
272 |
{ |
273 |
// Check all GOTO statements in the entire PROGRAM to see if they target the END..IF label |
274 |
// number selected during the Refactoring. If so then retarget/renumber them to the |
275 |
// CONTINUE block that follows. (Note: Inner GOTO statements that that target the END..IF |
276 |
// statement are not retargeted/renumbered.) |
277 |
for (ASTGotoStmtNode gotoNode : gotoStatementWithEndIfLabel) |
278 |
{ |
279 |
// Do not re-target GOTO statements from within the selected END..IF for refactoring. |
280 |
if (!selectedGotoStatementWithEndIfLabel.contains(gotoNode)) |
281 |
{ // Goto targeted to our selected IF..ENDIF block |
282 |
gotoNode.getGotoLblRef().getLabel() |
283 |
.setText(continueAfterIfStmtNode.getLabel().getText()); // Use existing |
284 |
} |
285 |
} |
286 |
|
287 |
// Label of selected END..IF for Refactoring is only removed after all other GOTOs have been |
288 |
// retargeted and if no inner GOTO statements target its label. |
289 |
if (selectedGotoStatementWithEndIfLabel.isEmpty()) |
290 |
{ |
291 |
selectedEndIfNode.setLabel(null); |
292 |
selectedEndIfNode.findFirstToken().setWhiteBefore( |
293 |
selectedIfConstructNode.findFirstToken().getWhiteBefore()); // reindenter alt. |
294 |
} |
295 |
|
296 |
} |
297 |
|
298 |
/** |
299 |
* Modify Fortran code to add a CONTINUE statement after labeled END IF then remove END IF label |
300 |
* only if there are no GOTO statements within the selected IF statement that target that END IF |
301 |
* statement. |
302 |
* |
303 |
*/ |
304 |
private void changeNoContinueAfterEndIf() |
305 |
{ |
306 |
|
307 |
// build the CONTINUE node |
308 |
if (continueAfterIfStmtNode != null) { return; } |
309 |
|
310 |
@SuppressWarnings("unchecked") |
311 |
ASTListNode<ASTNode> listNode = (ASTListNode<ASTNode>)selectedIfConstructNode.getParent(); |
312 |
|
313 |
// build the CONTINUE statement program source code |
314 |
String programString = "program p\n" + selectedEndIfNode.getLabel().getText() + " CONTINUE" //$NON-NLS-1$ //$NON-NLS-2$ |
315 |
+ EOL + "end program"; //$NON-NLS-1$ |
316 |
ASTMainProgramNode programNode = (ASTMainProgramNode)parseLiteralProgramUnit(programString); |
317 |
ASTContinueStmtNode continueStmt = (ASTContinueStmtNode)programNode.getBody().get(0); |
318 |
|
319 |
// insert into AST |
320 |
continueStmt.setParent(selectedIfConstructNode.getParent()); |
321 |
listNode.insertAfter(selectedIfConstructNode, continueStmt); |
322 |
|
323 |
// clear label on END IF statement |
324 |
if (selectedGotoStatementWithEndIfLabel.size() == 0) |
325 |
{ |
326 |
selectedEndIfNode.setLabel(null); |
327 |
// correct indentation |
328 |
selectedEndIfNode.findFirstToken().setWhiteBefore( |
329 |
selectedIfConstructNode.findFirstToken().getWhiteBefore()); |
330 |
} |
331 |
else |
332 |
{ |
333 |
// Grab all labels |
334 |
LinkedList<IActionStmt> actionStmts = getActionStmts(ScopingNode |
335 |
.getLocalScope(selectedEndIfNode)); |
336 |
|
337 |
// Calculate unique label |
338 |
String label = getUniqueLabel(actionStmts); |
339 |
|
340 |
// Set continue label to new label |
341 |
continueStmt.getLabel().setText(label); |
342 |
|
343 |
// Set all goto statements to new label |
344 |
for (ASTGotoStmtNode node : gotoStatementWithEndIfLabel) |
345 |
{ |
346 |
if (!selectedGotoStatementWithEndIfLabel.contains(node)) |
347 |
{ |
348 |
node.getGotoLblRef().getLabel().setText(label); |
349 |
} |
350 |
} |
351 |
} |
352 |
} |
353 |
|
354 |
/** |
355 |
* Build a list of all GOTO node types from the starting node. |
356 |
* |
357 |
* @param startNode Node to start the search. |
358 |
* |
359 |
* @return LinkedList of GOTO nodes. |
360 |
*/ |
361 |
private LinkedList<ASTGotoStmtNode> getGotoNodes(IASTNode startNode) |
362 |
{ |
363 |
if (startNode == null) { return null; } |
364 |
|
365 |
/* |
366 |
* Unable to find all goto statements when there are do loops in startNode, so need to |
367 |
* search in the do loops separately. |
368 |
*/ |
369 |
final LinkedList<ASTGotoStmtNode> gotoNodes = getGotoStmtsInAllProperLoopConstructs(startNode); |
370 |
|
371 |
startNode.accept(new ASTVisitor() |
372 |
{ |
373 |
@Override |
374 |
public void visitASTGotoStmtNode(ASTGotoStmtNode node) |
375 |
{ |
376 |
gotoNodes.add(node); |
377 |
} |
378 |
}); |
379 |
|
380 |
return gotoNodes; |
381 |
} |
382 |
|
383 |
/** |
384 |
* Build a list of all proper-loop-construct node types from the starting node. |
385 |
* |
386 |
* @param startNode Node to start the search. |
387 |
* @return LinkedList of proper-loop-construct nodes. |
388 |
*/ |
389 |
private LinkedList<ASTGotoStmtNode> getGotoStmtsInAllProperLoopConstructs(IASTNode startNode) |
390 |
{ |
391 |
final LinkedList<ASTGotoStmtNode> gotoNodes = new LinkedList<ASTGotoStmtNode>(); |
392 |
final LinkedList<ASTProperLoopConstructNode> loopNodes = getProperLoopConstructs(startNode); |
393 |
|
394 |
for (ASTProperLoopConstructNode loop : loopNodes) |
395 |
{ |
396 |
for (IASTNode node : loop.getBody()) |
397 |
{ |
398 |
node.accept(new ASTVisitor() |
399 |
{ |
400 |
@Override |
401 |
public void visitASTGotoStmtNode(ASTGotoStmtNode node) |
402 |
{ |
403 |
gotoNodes.add(node); |
404 |
} |
405 |
}); |
406 |
} |
407 |
} |
408 |
|
409 |
return gotoNodes; |
410 |
} |
411 |
|
412 |
/** |
413 |
* Find GOTO node(s) based on matching label. |
414 |
* |
415 |
* @param gotos List of GOTO statements to check for matching label. |
416 |
* @param label Label to match against. |
417 |
* @return List of GOTO nodes that target the specified label. |
418 |
*/ |
419 |
private LinkedList<ASTGotoStmtNode> findGotoForLabel(LinkedList<ASTGotoStmtNode> gotos, |
420 |
String label) |
421 |
{ |
422 |
if (gotos == null) { return new LinkedList<ASTGotoStmtNode>(); } |
423 |
|
424 |
LinkedList<ASTGotoStmtNode> gotoWithLabel = new LinkedList<ASTGotoStmtNode>(); |
425 |
for (ASTGotoStmtNode gotoNode : gotos) |
426 |
{ |
427 |
if (gotoNode.getGotoLblRef().getLabel().getText().contentEquals(label)) |
428 |
{ |
429 |
gotoWithLabel.add(gotoNode); |
430 |
} |
431 |
} |
432 |
|
433 |
return gotoWithLabel; |
434 |
} |
435 |
|
436 |
/** |
437 |
* Check for CONTINUE statement after a if construct node |
438 |
* |
439 |
* @param ifStmt If construct node. |
440 |
* @return continue statement node or null. |
441 |
*/ |
442 |
private static ASTContinueStmtNode continueAfterIfStmt(ASTIfConstructNode ifStmt) |
443 |
{ |
444 |
if (ifStmt == null) { return null; } |
445 |
|
446 |
@SuppressWarnings("unchecked") |
447 |
ASTListNode<ASTNode> list = (ASTListNode<ASTNode>)ifStmt.getParent(); |
448 |
|
449 |
for (int i = 0; i < list.size() - 1; i++) |
450 |
{ |
451 |
if (list.get(i) != null) |
452 |
{ |
453 |
if (list.get(i).equals(ifStmt)) |
454 |
{ |
455 |
if (list.get(i + 1) instanceof ASTContinueStmtNode) { return (ASTContinueStmtNode)list |
456 |
.get(i + 1); } |
457 |
} |
458 |
} |
459 |
} |
460 |
|
461 |
return null; |
462 |
} |
463 |
|
464 |
/** |
465 |
* Generate a unique label based on the labels passed in (unique in that find the largest and |
466 |
* add 10) |
467 |
* |
468 |
* @param actionStmts List of statements that may be labeled to consider for a unique label. |
469 |
* @return Unique label value. |
470 |
*/ |
471 |
private String getUniqueLabel(LinkedList<IActionStmt> actionStmts) |
472 |
{ |
473 |
int label = Integer.parseInt(selectedEndIfNode.getLabel().getText()); |
474 |
for (IActionStmt stmt : actionStmts) |
475 |
{ |
476 |
if (stmt.getLabel() != null) |
477 |
{ |
478 |
int currentLabel = Integer.parseInt(stmt.getLabel().getText()); |
479 |
if (currentLabel > label) |
480 |
{ |
481 |
label = currentLabel; |
482 |
} |
483 |
} |
484 |
} |
485 |
label += 10; |
486 |
|
487 |
return String.valueOf(label); |
488 |
} |
489 |
|
490 |
/** |
491 |
* Find Action statement nodes in the tree from the starting search node. |
492 |
* |
493 |
* @param startNode Starting node from which to perform the search. |
494 |
* @return LinkedList of action statement nodes that are found. |
495 |
*/ |
496 |
private LinkedList<IActionStmt> getActionStmts(IASTNode startNode) |
497 |
{ |
498 |
final LinkedList<IActionStmt> actionStmts = getActionStmtsInAllProperLoopConstructs(); |
499 |
startNode.accept(new ASTVisitor() |
500 |
{ |
501 |
@Override |
502 |
public void visitIActionStmt(IActionStmt node) |
503 |
{ |
504 |
actionStmts.add(node); |
505 |
} |
506 |
}); |
507 |
|
508 |
return actionStmts; |
509 |
} |
510 |
|
511 |
/** |
512 |
* Find Action statement nodes in entire file in editor. |
513 |
* |
514 |
* @return LinkedList of action statement nodes found. |
515 |
*/ |
516 |
private LinkedList<IActionStmt> getActionStmtsInAllProperLoopConstructs() |
517 |
{ |
518 |
final LinkedList<IActionStmt> actionStmts = new LinkedList<IActionStmt>(); |
519 |
final LinkedList<ASTProperLoopConstructNode> loopNodes = getProperLoopConstructs(ScopingNode |
520 |
.getLocalScope(selectedEndIfNode)); |
521 |
|
522 |
for (ASTProperLoopConstructNode loop : loopNodes) |
523 |
{ |
524 |
for (IASTNode node : loop.getBody()) |
525 |
{ |
526 |
node.accept(new ASTVisitor() |
527 |
{ |
528 |
@Override |
529 |
public void visitIActionStmt(IActionStmt node) |
530 |
{ |
531 |
actionStmts.add(node); |
532 |
} |
533 |
}); |
534 |
} |
535 |
} |
536 |
|
537 |
return actionStmts; |
538 |
} |
539 |
|
540 |
/** |
541 |
* Find all proper loop constructs in the tree from the starting search node. |
542 |
* |
543 |
* @param startNode Starting node from which to perform the search. |
544 |
* @return LinkedList of proper loop construct nodes found. |
545 |
*/ |
546 |
private LinkedList<ASTProperLoopConstructNode> getProperLoopConstructs(IASTNode startNode) |
547 |
{ |
548 |
final LinkedList<ASTProperLoopConstructNode> loopNodes = new LinkedList<ASTProperLoopConstructNode>(); |
549 |
|
550 |
// Change AST to represent DO-loops as ASTProperLoopConstructNodes |
551 |
LoopReplacer.replaceAllLoopsIn(this.astOfFileInEditor.getRoot()); |
552 |
|
553 |
startNode.accept(new ASTVisitorWithLoops() |
554 |
{ |
555 |
@Override |
556 |
public void visitASTProperLoopConstructNode(ASTProperLoopConstructNode node) |
557 |
{ |
558 |
loopNodes.add(node); |
559 |
} |
560 |
}); |
561 |
|
562 |
return loopNodes; |
563 |
} |
564 |
} |