1010use PhpParser \Node \Stmt ;
1111use PhpParser \NodeVisitor ;
1212use Rector \Application \RectorRegistry ;
13+ use Rector \Exception \ShouldNotHappenException ;
14+ use Rector \PhpParser \Printer \BetterStandardPrinter ;
1315use Rector \Rector \RectorRunner ;
1416use Rector \ValueObject \Application \File ;
1517
1618final class RectorNodeTraverser
1719{
20+ /**
21+ * Safety cap on local re-visits of a single node, so a rule that never settles cannot loop forever.
22+ */
23+ private const int MAX_REVISITS = 50 ;
24+
1825 private bool $ stopTraversal ;
1926
2027 public function __construct (
2128 private readonly RectorRunner $ rectorRunner ,
2229 private readonly RectorRegistry $ rectorRegistry ,
30+ private readonly BetterStandardPrinter $ betterStandardPrinter ,
2331 ) {
2432 }
2533
@@ -51,31 +59,8 @@ private function traverseNode(Node $node, File $file): void
5159 continue ;
5260 }
5361
54- $ currentNodeVisitors = $ this ->rectorRegistry ->forNodeAndFile ($ subNode , $ file );
55-
56- foreach ($ currentNodeVisitors as $ currentNodeVisitor ) {
57- $ return = $ this ->rectorRunner ->run ($ currentNodeVisitor , $ subNode );
58- if ($ return === null ) {
59- continue ;
60- }
61-
62- if ($ return instanceof Node) {
63- $ originalSubNodeClass = $ subNode ::class;
64-
65- $ this ->ensureReplacementReasonable ($ subNode , $ return );
66- $ subNode = $ return ;
67- $ node ->{$ name } = $ return ;
68-
69- if ($ originalSubNodeClass !== $ subNode ::class) {
70- // stop traversing as node type changed and visitors won't work
71- continue 2 ;
72- }
73- } else {
74- throw new LogicException ('RectorRunner::run() returned invalid value of type ' . gettype ($ return ));
75- }
76- }
77-
78- $ this ->traverseNode ($ subNode , $ file );
62+ // a single sub-node slot cannot hold a list or be removed
63+ $ node ->{$ name } = $ this ->refactorNode ($ subNode , $ file , false );
7964
8065 if ($ this ->stopTraversal ) {
8166 break ;
@@ -99,33 +84,16 @@ private function traverseArray(array $nodes, File $file): array
9984 continue ;
10085 }
10186
102- $ currentNodeVisitors = $ this ->rectorRegistry ->forNodeAndFile ($ node , $ file );
103-
104- foreach ($ currentNodeVisitors as $ currentNodeVisitor ) {
105- $ return = $ this ->rectorRunner ->run ($ currentNodeVisitor , $ node );
106- if ($ return !== null ) {
107- if ($ return instanceof Node) {
108- $ originalNodeNodeClass = $ node ::class;
109- $ this ->ensureReplacementReasonable ($ node , $ return );
110- $ nodes [$ i ] = $ node = $ return ;
111-
112- if ($ originalNodeNodeClass !== $ return ::class) {
113- // stop traversing as node type changed and visitors won't work
114- continue 2 ;
115- }
116- } elseif (\is_array ($ return )) {
117- $ doNodes [] = [$ i , $ return ];
118- continue 2 ;
119- } elseif ($ return === NodeVisitor::REMOVE_NODE ) {
120- $ doNodes [] = [$ i , []];
121- continue 2 ;
122- } else {
123- throw new LogicException ('RectorRunner::run() returned invalid value of type ' . gettype ($ return ));
124- }
125- }
87+ $ result = $ this ->refactorNode ($ node , $ file , true );
88+
89+ if (\is_array ($ result )) {
90+ $ doNodes [] = [$ i , $ result ];
91+ } elseif ($ result === NodeVisitor::REMOVE_NODE ) {
92+ $ doNodes [] = [$ i , []];
93+ } else {
94+ $ nodes [$ i ] = $ result ;
12695 }
12796
128- $ this ->traverseNode ($ node , $ file );
12997 if ($ this ->stopTraversal ) {
13098 break ;
13199 }
@@ -140,6 +108,114 @@ private function traverseArray(array $nodes, File $file): array
140108 return $ nodes ;
141109 }
142110
111+ /**
112+ * Applies the node's rules and traverses into its children, then re-applies its rules while a
113+ * descendant keeps changing - so a rule that depends on a change made deeper in the tree (ordered
114+ * via OrderedRectorInterface, or a plain multi-pass chain) converges within a single file pass.
115+ *
116+ * Termination is structural: the loop stops once a round reports no change, or reports a change
117+ * that leaves the node's printed form identical (a rule that keeps returning the same result).
118+ *
119+ * @param bool $allowListReturn whether an array / REMOVE_NODE return is valid here (only for a
120+ * node that lives inside a list, not a single sub-node slot)
121+ * @return NodeVisitor::REMOVE_NODE|Node|Node[]
122+ */
123+ private function refactorNode (Node $ node , File $ file , bool $ allowListReturn ): int |Node |array
124+ {
125+ $ lastPrint = null ;
126+ $ revisits = 0 ;
127+
128+ while (true ) {
129+ $ changeCountBefore = $ this ->changeCount ($ file );
130+ $ return = $ this ->applyRectors ($ node , $ file );
131+
132+ if (\is_array ($ return ) || $ return === NodeVisitor::REMOVE_NODE ) {
133+ if (! $ allowListReturn ) {
134+ throw new LogicException ('RectorRunner::run() returned invalid value of type ' . gettype ($ return ));
135+ }
136+
137+ // a list / removal replaces the node in its parent list; its children are not traversed
138+ return $ return ;
139+ }
140+
141+ $ typeChanged = $ return ::class !== $ node ::class;
142+ $ node = $ return ;
143+
144+ if ($ typeChanged ) {
145+ // stop traversing as node type changed and visitors won't work
146+ return $ node ;
147+ }
148+
149+ $ this ->traverseNode ($ node , $ file );
150+ if ($ this ->stopTraversal ) {
151+ return $ node ;
152+ }
153+
154+ // no rule (here or deeper) reported a change this round: the subtree is stable
155+ if ($ this ->changeCount ($ file ) === $ changeCountBefore ) {
156+ return $ node ;
157+ }
158+
159+ // a change was reported; keep going only while it actually reshapes the node
160+ $ printNow = $ this ->betterStandardPrinter ->print ($ node );
161+ if ($ printNow === $ lastPrint ) {
162+ return $ node ;
163+ }
164+
165+ $ lastPrint = $ printNow ;
166+
167+ if (++$ revisits >= self ::MAX_REVISITS ) {
168+ throw new ShouldNotHappenException (
169+ sprintf ('Node "%s" did not converge after %d re-visits ' , $ node ->getType (), $ revisits )
170+ );
171+ }
172+ }
173+ }
174+
175+ /**
176+ * Runs every rule subscribed to this node, in order. A rule returning a same-type node keeps the
177+ * remaining rules running on the new node; a type change, an array or REMOVE_NODE stops the chain
178+ * and is handed back to the caller.
179+ *
180+ * @return NodeVisitor::REMOVE_NODE|Node|Node[]
181+ */
182+ private function applyRectors (Node $ node , File $ file ): int |Node |array
183+ {
184+ $ currentNodeVisitors = $ this ->rectorRegistry ->forNodeAndFile ($ node , $ file );
185+
186+ foreach ($ currentNodeVisitors as $ currentNodeVisitor ) {
187+ $ return = $ this ->rectorRunner ->run ($ currentNodeVisitor , $ node );
188+ if ($ return === null ) {
189+ continue ;
190+ }
191+
192+ if ($ return instanceof Node) {
193+ $ originalNodeClass = $ node ::class;
194+ $ this ->ensureReplacementReasonable ($ node , $ return );
195+ $ node = $ return ;
196+
197+ if ($ originalNodeClass !== $ node ::class) {
198+ return $ node ;
199+ }
200+
201+ continue ;
202+ }
203+
204+ if (\is_array ($ return ) || $ return === NodeVisitor::REMOVE_NODE ) {
205+ return $ return ;
206+ }
207+
208+ throw new LogicException ('RectorRunner::run() returned invalid value of type ' . gettype ($ return ));
209+ }
210+
211+ return $ node ;
212+ }
213+
214+ private function changeCount (File $ file ): int
215+ {
216+ return count ($ file ->getRectorWithLineChanges ());
217+ }
218+
143219 private function ensureReplacementReasonable (Node $ old , Node $ new ): void
144220 {
145221 if ($ old instanceof Stmt) {
0 commit comments