Skip to content

Commit 3999e3e

Browse files
committed
[dx] immutable node visitor
1 parent d48ab7c commit 3999e3e

File tree

5 files changed

+587
-18
lines changed

5 files changed

+587
-18
lines changed

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,7 @@
133133
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-if-php.patch",
134134
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-case-php.patch",
135135
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-elseif-php.patch",
136-
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch",
137-
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-nodetraverser-php.patch"
136+
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch"
138137
]
139138
},
140139
"composer-exit-on-patch-failure": true,

phpstan.neon

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ parameters:
217217

218218
# optional as changes behavior, should be used explicitly outside PHP upgrade
219219
- '#Register "Rector\\Php73\\Rector\\FuncCall\\JsonThrowOnErrorRector" service to "php73\.php" config set#'
220-
- '#Register "Rector\\Php81\\Rector\\ClassMethod\\NewInInitializerRector" service to "php81\.php" config set#'
221220
- '#Register "Rector\\Php80\\Rector\\NotIdentical\\MbStrContainsRector" service to "php80\.php" config set#'
222221
- '#Register "Rector\\Php85\\Rector\\StmtsAwareInterface\\SequentialAssignmentsToPipeOperatorRector" service to "php85\.php" config set#'
223222
- '#Register "Rector\\Php85\\Rector\\Expression\\NestedFuncCallsToPipeOperatorRector" service to "php85\.php" config set#'
@@ -356,6 +355,27 @@ parameters:
356355

357356
- '#Method Rector\\Utils\\Rector\\RemoveRefactorDuplicatedNodeInstanceCheckRector\:\:getInstanceofNodeClass\(\) should return class\-string<PhpParser\\Node>\|null but returns class\-string#'
358357

358+
# helper classes for bin script
359359
-
360-
path: bin/list-unused-rules.php
360+
path: scripts
361361
identifier: symplify.multipleClassLikeInFile
362+
363+
# copied from /vendor, to keep as original as possible
364+
-
365+
identifier: symplify.forbiddenNode
366+
path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php
367+
-
368+
identifier: missingType.iterableValue
369+
path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php
370+
-
371+
identifier: symplify.noDynamicName
372+
path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php
373+
-
374+
identifier: offsetAccess.nonArray
375+
path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php
376+
-
377+
message: '#Property PhpParser\\Node\\Stmt\\ClassMethod\:\:\$stmts \(array<PhpParser\\Node\\Stmt>\|null\) does not accept array<PhpParser\\Node>#'
378+
path: scripts/copy-and-create-immuatable-node-visitor.php
379+
-
380+
message: '#Property Rector\\PhpParser\\NodeTraverser\\AbstractImmutableNodeTraverser\:\:\$visitors \(list<PhpParser\\NodeVisitor>\) does not accept array<int\|string, PhpParser\\NodeVisitor>#'
381+
path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php

scripts/copy-and-create-immuatable-node-visitor.php

Lines changed: 251 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,260 @@
22

33
declare(strict_types=1);
44

5+
use Nette\Utils\FileSystem;
6+
use PhpParser\Modifiers;
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr;
10+
use PhpParser\Node\Expr\ArrayDimFetch;
11+
use PhpParser\Node\Expr\Assign;
12+
use PhpParser\Node\Expr\MethodCall;
13+
use PhpParser\Node\Expr\PropertyFetch;
14+
use PhpParser\Node\Expr\Variable;
15+
use PhpParser\Node\Identifier;
16+
use PhpParser\Node\Name;
17+
use PhpParser\Node\Name\FullyQualified;
18+
use PhpParser\Node\Param;
19+
use PhpParser\Node\Stmt;
20+
use PhpParser\Node\Stmt\Class_;
21+
use PhpParser\Node\Stmt\ClassMethod;
22+
use PhpParser\Node\Stmt\Expression;
23+
use PhpParser\Node\Stmt\Foreach_;
24+
use PhpParser\Node\Stmt\Namespace_;
25+
use PhpParser\Node\Stmt\Use_;
26+
use PhpParser\Node\UseItem;
27+
use PhpParser\NodeTraverser;
28+
use PhpParser\NodeTraverserInterface;
29+
use PhpParser\NodeVisitor;
30+
use PhpParser\NodeVisitorAbstract;
31+
use PhpParser\PrettyPrinter\Standard;
32+
533
require __DIR__ . '/../vendor/autoload.php';
634

35+
// load file contents from vendor
36+
// modify clas name + namespace
37+
// add it here as "AbstractImmutableNodeTraverser.php"
738

8-
// 1. copy file from vendor
9-
// load file contents from venodr
39+
$vendorNodeTraverserFilePath = __DIR__ . '/../vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php';
1040

11-
// modify clas name + namespace
41+
$parserFactory = new \PhpParser\ParserFactory();
42+
$parser = $parserFactory->createForHostVersion();
43+
$stmts = $parser->parse(FileSystem::read($vendorNodeTraverserFilePath));
44+
45+
\Webmozart\Assert\Assert::isArray($stmts);
46+
\Webmozart\Assert\Assert::allIsInstanceOf($stmts, Stmt::class);
47+
48+
$originalStmts = $stmts;
49+
50+
final class ReplaceForeachThisVisitorNodeVisitor extends NodeVisitorAbstract
51+
{
52+
public function __construct(
53+
private readonly string $nodeName
54+
) {
55+
56+
}
57+
58+
/**
59+
* @return Stmt[]|null
60+
*/
61+
public function enterNode(Node $node): ?array
62+
{
63+
if (! $node instanceof Foreach_) {
64+
return null;
65+
}
66+
67+
if (! $node->expr instanceof PropertyFetch) {
68+
return null;
69+
}
70+
71+
$foreachedPropertyFetch = $node->expr;
72+
if (! $foreachedPropertyFetch->var instanceof Variable) {
73+
return null;
74+
}
75+
76+
if ($foreachedPropertyFetch->var->name !== 'this') {
77+
return null;
78+
}
79+
80+
if (! $foreachedPropertyFetch->name instanceof Identifier) {
81+
return null;
82+
}
83+
84+
if ($foreachedPropertyFetch->name->toString() !== 'visitors') {
85+
return null;
86+
}
87+
88+
// replace $this->visitors with $currentNodeVisitors
89+
$currentNodeVisitorsVariable = new Variable('currentNodeVisitors');
90+
$node->expr = $currentNodeVisitorsVariable;
91+
92+
// add before foreach: $currentNodeVisitors = $this->getVisitorsForNode($node);
93+
$assign = new Assign(
94+
$currentNodeVisitorsVariable,
95+
new MethodCall(
96+
new Variable('this'),
97+
ImmutableNodeTraverserName::GET_VISITORS_FOR_NODE_METHOD,
98+
[new Arg(new Variable($this->nodeName))]
99+
)
100+
);
101+
102+
return [new Expression($assign), $node];
103+
}
104+
}
105+
106+
final class ReplaceThisVisitorsWithThisGetVisitorsNodeVisitor extends NodeVisitorAbstract
107+
{
108+
public function enterNode(Node $node): ?ClassMethod
109+
{
110+
if (! $node instanceof ClassMethod || $node->stmts === null) {
111+
return null;
112+
}
113+
114+
if (! in_array($node->name->toString(), ['traverseArray', 'traverseNode'])) {
115+
return null;
116+
}
117+
118+
if ($node->name->toString() === 'traverseNode') {
119+
$traverseArrayNodeName = 'subNode';
120+
} else {
121+
$traverseArrayNodeName = 'node';
122+
}
123+
124+
// handle foreach $this->visitors
125+
$nodeTraverser = new NodeTraverser();
126+
$nodeTraverser->addVisitor(new ReplaceForeachThisVisitorNodeVisitor($traverseArrayNodeName));
127+
128+
$node->stmts = $nodeTraverser->traverse((array) $node->stmts);
129+
130+
return $node;
131+
}
132+
}
133+
134+
final class RenameNamespaceNodeVisitor extends NodeVisitorAbstract
135+
{
136+
public function enterNode(Node $node): ?Namespace_
137+
{
138+
if (! $node instanceof Namespace_) {
139+
return null;
140+
}
141+
142+
// add uses for PHPParser nodes as locations are now changed
143+
$uses = [
144+
new Use_([new UseItem(new Name(NodeTraverserInterface::class))]),
145+
new Use_([new UseItem(new Name(NodeVisitor::class))]),
146+
new Use_([new UseItem(new Name(Node::class))]),
147+
];
148+
149+
/** @var Stmt[] $newStmts */
150+
$newStmts = array_merge($uses, (array) $node->stmts);
151+
$node->stmts = $newStmts;
152+
153+
$node->name = new Name('Rector\PhpParser\NodeTraverser');
154+
return $node;
155+
}
156+
}
157+
158+
final class ReplaceThisVisitorsArrayDimFetchWithCurrentNodeVisitorsNodeVisitor extends NodeVisitorAbstract
159+
{
160+
public function enterNode(Node $node): ?Node
161+
{
162+
if (! $node instanceof ArrayDimFetch) {
163+
return null;
164+
}
165+
166+
if (! $node->var instanceof PropertyFetch) {
167+
return null;
168+
}
169+
170+
$propertyFetch = $node->var;
171+
if (! $propertyFetch->var instanceof Variable) {
172+
return null;
173+
}
174+
175+
if ($propertyFetch->var->name !== 'this') {
176+
return null;
177+
}
178+
179+
if (! $propertyFetch->name instanceof Identifier) {
180+
return null;
181+
}
182+
183+
if ($propertyFetch->name->toString() !== 'visitors') {
184+
return null;
185+
}
186+
187+
if (! $node->dim instanceof Variable) {
188+
return null;
189+
}
190+
191+
if ($node->dim->name !== 'visitorIndex') {
192+
return null;
193+
}
194+
195+
$node->var = new Variable('currentNodeVisitors');
196+
197+
return $node;
198+
}
199+
}
200+
201+
final class DecorateClassNodeVisitor extends NodeVisitorAbstract
202+
{
203+
public function enterNode(Node $node): ?Class_
204+
{
205+
if (! $node instanceof Class_) {
206+
return null;
207+
}
208+
209+
$node->flags |= Modifiers::ABSTRACT;
210+
$node->name = new Identifier('AbstractImmutableNodeTraverser');
211+
212+
$getVisitorsForNodeClassMethod = new ClassMethod(ImmutableNodeTraverserName::GET_VISITORS_FOR_NODE_METHOD, [
213+
'flags' => Modifiers::PUBLIC | Modifiers::ABSTRACT,
214+
'params' => [new Param(var: new Expr\Variable('node'), type: new FullyQualified(Node::class))],
215+
'returnType' => new Node\Name('array'),
216+
'stmts' => null,
217+
]);
218+
219+
// add @return NodeVisitor[] docblock
220+
$getVisitorsForNodeClassMethod->setDocComment(new \PhpParser\Comment\Doc(
221+
<<<'DOC'
222+
/**
223+
* @return NodeVisitor[]
224+
*/
225+
DOC
226+
));
227+
228+
$node->stmts[] = $getVisitorsForNodeClassMethod;
229+
230+
return $node;
231+
}
232+
}
233+
234+
$nodeTraverser = new NodeTraverser();
235+
236+
$nodeTraverser->addVisitor(new DecorateClassNodeVisitor());
237+
$nodeTraverser->addVisitor(new ReplaceThisVisitorsWithThisGetVisitorsNodeVisitor());
238+
$nodeTraverser->addVisitor(new ReplaceThisVisitorsArrayDimFetchWithCurrentNodeVisitorsNodeVisitor());
239+
$nodeTraverser->addVisitor(new RenameNamespaceNodeVisitor());
240+
241+
$stmts = $nodeTraverser->traverse($stmts);
242+
243+
final class ImmutableNodeTraverserName
244+
{
245+
/**
246+
* @var string
247+
*/
248+
public const GET_VISITORS_FOR_NODE_METHOD = 'getVisitorsForNode';
249+
}
250+
251+
// print node traverser contents
252+
$standard = new Standard();
253+
$immutableNodeTraverserFileContents = $standard->printFormatPreserving($stmts, $originalStmts, $parser->getTokens());
12254

13-
// add it here as ImmutableNodeTraverser.php
255+
// save the file
256+
FileSystem::write(
257+
__DIR__ . '/../src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php',
258+
$immutableNodeTraverserFileContents
259+
);
14260

15-
\Nette\Utils\FileSystem::copy(__DIR__ . '/../vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php', __DIR__ . '/../src/PhpParser/NodeTraverser/ImmutableNodeTraverser.php');
261+
echo sprintf('New file "%s" was created', 'src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php') . PHP_EOL;

0 commit comments

Comments
 (0)