@@ -61,6 +61,7 @@ protected function configure(): void
61
61
{
62
62
$ this ->setName ('extract ' );
63
63
$ this ->addOption ('update ' , null , InputOption::VALUE_NONE );
64
+ $ this ->addArgument ('updateFrom ' , InputArgument::OPTIONAL );
64
65
$ this ->addArgument ('updateTo ' , InputArgument::OPTIONAL );
65
66
}
66
67
@@ -72,10 +73,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
72
73
throw new \LogicException ('Invalid stubs path ' );
73
74
}
74
75
76
+ $ updateFrom = $ input ->getArgument ('updateFrom ' );
75
77
$ updateTo = $ input ->getArgument ('updateTo ' );
76
78
if (!$ isUpdate ) {
77
79
$ this ->clearOldStubs ($ ourStubsDir );
78
80
} else {
81
+ if ($ updateFrom === null ) {
82
+ throw new \LogicException ('Missing arguments ' );
83
+ }
79
84
if ($ updateTo === null ) {
80
85
throw new \LogicException ('Missing arguments ' );
81
86
}
@@ -92,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
92
97
$ addFunctions = [];
93
98
foreach ($ finder as $ file ) {
94
99
$ stubPath = $ file ->getRealPath ();
95
- [$ tmpClasses , $ tmpFunctions ] = $ this ->extractStub ($ stubPath , $ file ->getRelativePathname (), $ isUpdate , $ updateTo );
100
+ [$ tmpClasses , $ tmpFunctions ] = $ this ->extractStub ($ stubPath , $ file ->getRelativePathname (), $ isUpdate , $ updateFrom , $ updateTo );
96
101
foreach ($ tmpClasses as $ className => $ fileName ) {
97
102
$ addClasses [$ className ] = $ fileName ;
98
103
}
@@ -108,7 +113,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
108
113
$ addFunctions = [];
109
114
} else {
110
115
require_once __DIR__ . '/../Php8StubsMap.php ' ;
111
- $ map = new \PHPStan \Php8StubsMap (80000 ); // todo "from" argument when updating from 8.1 to 8.2 for example
116
+ $ parts = explode ('. ' , $ updateFrom );
117
+ $ map = new \PHPStan \Php8StubsMap ((int ) $ parts [0 ] * 10000 + (int ) ($ parts [1 ] ?? 0 ) * 100 + (int ) ($ parts [2 ] ?? 0 ));
112
118
$ classes = $ map ->classes ;
113
119
$ functions = $ map ->functions ;
114
120
foreach ($ addClasses as $ className => $ fileName ) {
@@ -117,7 +123,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
117
123
}
118
124
119
125
if ($ classes [$ className ] !== $ fileName ) {
120
- throw new \LogicException (sprintf ('File name of class %s changed from %s to %s. ' , $ className , $ classes [$ className ], $ fileName ));
126
+ $ addClasses [$ className ] = $ fileName ;
127
+ continue ;
121
128
}
122
129
123
130
unset($ addClasses [$ className ]);
@@ -128,7 +135,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
128
135
}
129
136
130
137
if ($ functions [$ functionName ] !== $ fileName ) {
131
- throw new \LogicException (sprintf ('File name of function %s changed from %s to %s. ' , $ functionName , $ functions [$ functionName ], $ fileName ));
138
+ $ addFunctions [$ functionName ] = $ fileName ;
139
+ continue ;
132
140
}
133
141
134
142
unset($ addFunctions [$ functionName ]);
@@ -164,11 +172,9 @@ private function clearOldStubs(string $ourStubsDir): void
164
172
}
165
173
166
174
/**
167
- * @param string $stubPath
168
- * @param string $relativeStubPath
169
175
* @return array{array<string, string>, array<string, string>}
170
176
*/
171
- private function extractStub (string $ stubPath , string $ relativeStubPath , bool $ isUpdate , ?string $ updateTo ): array
177
+ private function extractStub (string $ stubPath , string $ relativeStubPath , bool $ isUpdate , ?string $ updateFrom , ? string $ updateTo ): array
172
178
{
173
179
$ nameResolver = new PhpParser \NodeVisitor \NameResolver ;
174
180
$ nodeTraverser = new PhpParser \NodeTraverser ;
@@ -179,7 +185,7 @@ private function extractStub(string $stubPath, string $relativeStubPath, bool $i
179
185
private string $ stubPath ;
180
186
181
187
/** @var PhpParser\Node\Stmt[] */
182
- private array $ stmts ;
188
+ private array $ stmts = [] ;
183
189
184
190
public function __construct (string $ stubPath )
185
191
{
@@ -209,6 +215,10 @@ public function enterNode(Node $node)
209
215
// pass
210
216
} elseif ($ node instanceof Node \Name) {
211
217
// pass
218
+ } elseif ($ node instanceof Node \Stmt \Expression) {
219
+ return PhpParser \NodeTraverser::DONT_TRAVERSE_CHILDREN ;
220
+ } elseif ($ node instanceof Node \Stmt \Const_) {
221
+ return PhpParser \NodeTraverser::DONT_TRAVERSE_CHILDREN ;
212
222
} else {
213
223
throw new \Exception (sprintf ('Unhandled node type %s in %s on line %s. ' , get_class ($ node ), $ this ->stubPath , $ node ->getLine ()));
214
224
}
@@ -303,7 +313,7 @@ public function clear(): void
303
313
$ visitor ->clear ();
304
314
$ nodeTraverser ->traverse ($ oldStubAst );
305
315
306
- $ oldStmts = $ visitor ->getStmts ();
316
+ [ $ untouchedStmts , $ oldStmts] = $ this -> filterStatementsByVersion ( $ visitor ->getStmts (), $ updateFrom );
307
317
if (count ($ oldStmts ) !== 1 ) {
308
318
throw new \LogicException ('There is supposed to be one statement in the old AST: ' . $ targetStubPath );
309
319
}
@@ -317,24 +327,85 @@ public function clear(): void
317
327
$ oldStmt = new Node \Stmt \Namespace_ ($ oldStmt ->namespacedName ->slice (0 , -1 ), [$ oldStmt ]);
318
328
}
319
329
320
- $ newStmts = $ this ->compareStatements ($ oldStmt , $ stmt , $ updateTo );
321
- file_put_contents ($ targetStubPath , "<?php \n\n" . $ this ->printer ->prettyPrint ($ newStmts ));
330
+ $ newStmts = $ this ->compareStatements ($ oldStmt , $ stmt , $ updateFrom , $ updateTo );
331
+ file_put_contents ($ targetStubPath , "<?php \n\n" . $ this ->printer ->prettyPrint (array_merge ( $ untouchedStmts , $ newStmts) ));
322
332
}
323
333
324
334
return [$ classes , $ functions ];
325
335
}
326
336
337
+ /**
338
+ * @param Node\Stmt[] $stmts
339
+ * @return array{Node\Stmt[], Node\Stmt[]}
340
+ */
341
+ private function filterStatementsByVersion (array $ stmts , string $ updateFrom ): array
342
+ {
343
+ $ oldStmts = [];
344
+ $ newStmts = [];
345
+ $ parts = explode ('. ' , $ updateFrom );
346
+ $ phpVersionFrom = (int ) $ parts [0 ] * 10000 + (int ) ($ parts [1 ] ?? 0 ) * 100 + (int ) ($ parts [2 ] ?? 0 );
347
+ foreach ($ stmts as $ stmt ) {
348
+ if (!isset ($ stmt ->attrGroups )) {
349
+ $ newStmts [] = $ stmt ;
350
+ continue ;
351
+ }
352
+ $ attrGroups = $ stmt ->attrGroups ;
353
+ if (count ($ attrGroups ) === 0 ) {
354
+ $ newStmts [] = $ stmt ;
355
+ continue ;
356
+ }
357
+
358
+ $ since = null ;
359
+ $ until = null ;
360
+ foreach ($ attrGroups as $ attrGroup ) {
361
+ foreach ($ attrGroup ->attrs as $ attr ) {
362
+ if ($ attr ->name ->toLowerString () === 'since ' ) {
363
+ $ since = $ attr ;
364
+ continue ;
365
+ }
366
+ if ($ attr ->name ->toLowerString () === 'until ' ) {
367
+ $ until = $ attr ;
368
+ continue ;
369
+ }
370
+ }
371
+ }
372
+
373
+ $ sinceId = null ;
374
+ if ($ since !== null ) {
375
+ $ parts = explode ('. ' , $ since ->args [0 ]->value ->value );
376
+ $ sinceId = (int ) $ parts [0 ] * 10000 + (int ) ($ parts [1 ] ?? 0 ) * 100 + (int ) ($ parts [2 ] ?? 0 );
377
+ if ($ sinceId > $ phpVersionFrom ) {
378
+ $ oldStmts [] = $ stmt ;
379
+ continue ;
380
+ }
381
+ }
382
+ $ untilId = null ;
383
+ if ($ until !== null ) {
384
+ $ parts = explode ('. ' , $ until ->args [0 ]->value ->value );
385
+ $ untilId = ((int ) $ parts [0 ] * 10000 + (int ) ($ parts [1 ] ?? 0 ) * 100 + (int ) ($ parts [2 ] ?? 99 )) - 100 ;
386
+ if ($ untilId < $ phpVersionFrom ) {
387
+ $ oldStmts [] = $ stmt ;
388
+ continue ;
389
+ }
390
+ }
391
+
392
+ $ newStmts [] = $ stmt ;
393
+ }
394
+
395
+ return [$ oldStmts , $ newStmts ];
396
+ }
397
+
327
398
/** @return Node\Stmt[] */
328
- private function compareStatements (Node \Stmt $ old , Node \Stmt $ new , string $ updateTo ): array
399
+ private function compareStatements (Node \Stmt $ old , Node \Stmt $ new , string $ updateFrom , string $ updateTo ): array
329
400
{
330
401
if ($ old instanceof Node \Stmt \Namespace_ && $ new instanceof Node \Stmt \Namespace_) {
331
402
if ($ old ->name ->toString () !== $ new ->name ->toString ()) {
332
403
throw new \LogicException ('Namespace name changed ' );
333
404
}
334
405
335
- return [new Node \Stmt \Namespace_ ($ old ->name , $ this ->compareStatementsInNamespace ($ old ->stmts , $ new ->stmts , $ updateTo ))];
406
+ return [new Node \Stmt \Namespace_ ($ old ->name , $ this ->compareStatementsInNamespace ($ old ->stmts , $ new ->stmts , $ updateFrom , $ updateTo ))];
336
407
} elseif (!$ old instanceof Node \Stmt \Namespace_ && !$ new instanceof Node \Stmt \Namespace_) {
337
- return $ this ->compareStatementsInNamespace ([$ old ], [$ new ], $ updateTo );
408
+ return $ this ->compareStatementsInNamespace ([$ old ], [$ new ], $ updateFrom , $ updateTo );
338
409
}
339
410
340
411
throw new \LogicException ('Something about a namespace changed ' );
@@ -345,8 +416,9 @@ private function compareStatements(Node\Stmt $old, Node\Stmt $new, string $updat
345
416
* @param Node\Stmt[] $newStmts
346
417
* @return Node\Stmt[]
347
418
*/
348
- private function compareStatementsInNamespace (array $ oldStmts , array $ newStmts , string $ updateTo ): array
419
+ private function compareStatementsInNamespace (array $ oldStmts , array $ newStmts , string $ updateFrom , string $ updateTo ): array
349
420
{
421
+ [$ untouchedStmts , $ oldStmts ] = $ this ->filterStatementsByVersion ($ oldStmts , $ updateFrom );
350
422
if (count ($ oldStmts ) !== 1 || count ($ newStmts ) !== 1 ) {
351
423
throw new \LogicException ('There is supposed to be one statement in the AST ' );
352
424
}
@@ -358,27 +430,29 @@ private function compareStatementsInNamespace(array $oldStmts, array $newStmts,
358
430
if ($ old ->namespacedName ->toString () !== $ new ->namespacedName ->toString ()) {
359
431
throw new \LogicException ('Classname changed ' );
360
432
}
361
- $ new ->stmts = array_filter ($ new ->stmts , fn (Node \Stmt $ stmt ) => $ stmt instanceof Node \Stmt \ClassMethod);
433
+ $ newMethods = array_filter ($ new ->stmts , fn (Node \Stmt $ stmt ) => $ stmt instanceof Node \Stmt \ClassMethod);
434
+ [$ untouchedStmts , $ oldMethodsStmt ] = $ this ->filterStatementsByVersion ($ old ->stmts , $ updateFrom );
362
435
$ oldMethods = [];
363
- foreach ($ old -> stmts as $ stmt ) {
436
+ foreach ($ oldMethodsStmt as $ stmt ) {
364
437
if (!$ stmt instanceof Node \Stmt \ClassMethod) {
365
438
continue ;
366
439
}
367
440
368
441
$ oldMethods [$ stmt ->name ->toLowerString ()] = $ stmt ;
369
442
}
370
443
371
- if ($ new ->stmts !== null ) {
372
- $ newStmtsToSet = [] ;
373
- foreach ($ new -> stmts as $ i => $ stmt ) {
444
+ if ($ old ->stmts !== null ) {
445
+ $ newStmtsToSet = $ untouchedStmts ;
446
+ foreach ($ newMethods as $ stmt ) {
374
447
$ methodName = $ stmt ->name ->toLowerString ();
375
448
if (!array_key_exists ($ methodName , $ oldMethods )) {
376
- $ new -> stmts [ $ i ] ->attrGroups [] = new Node \AttributeGroup ([
449
+ $ stmt ->attrGroups [] = new Node \AttributeGroup ([
377
450
new Node \Attribute (
378
451
new Node \Name \FullyQualified ('Since ' ),
379
452
[new Node \Arg (new Node \Scalar \String_ ($ updateTo ))],
380
453
),
381
454
]);
455
+ $ newStmtsToSet [] = $ stmt ;
382
456
continue ;
383
457
}
384
458
@@ -389,10 +463,10 @@ private function compareStatementsInNamespace(array $oldStmts, array $newStmts,
389
463
390
464
// todo has a method been removed?
391
465
392
- $ new ->stmts = $ newStmtsToSet ;
466
+ $ old ->stmts = $ newStmtsToSet ;
393
467
}
394
468
395
- return [$ new ];
469
+ return [$ old ];
396
470
}
397
471
398
472
if ($ old instanceof Node \Stmt \Function_ && $ new instanceof Node \Stmt \Function_) {
@@ -416,14 +490,14 @@ private function compareFunctions(Node\FunctionLike $old, Node\FunctionLike $new
416
490
if ($ old ->getDocComment () !== null ) {
417
491
$ oldPhpDocNode = $ this ->parseDocComment ($ old ->getDocComment ()->getText ());
418
492
$ oldPhpDocReturn = $ this ->findPhpDocReturn ($ oldPhpDocNode );
419
- if ($ oldPhpDocNode !== null ) {
493
+ if ($ oldPhpDocReturn !== null ) {
420
494
$ newPhpDocNode = $ this ->parseDocComment ($ new ->getDocComment ()->getText ());
421
495
$ newPhpDocReturn = $ this ->findPhpDocReturn ($ newPhpDocNode );
422
496
if ($ newPhpDocReturn === null ) {
423
497
$ children = $ newPhpDocNode ->children ;
424
498
$ children [] = new PhpDocTagNode ('@return ' , $ oldPhpDocReturn );
425
499
$ newPhpDocNodeWithReturn = new PhpDocNode ($ children );
426
- $ new ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithReturn ));
500
+ $ old ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithReturn ));
427
501
}
428
502
}
429
503
}
@@ -492,15 +566,15 @@ private function compareFunctions(Node\FunctionLike $old, Node\FunctionLike $new
492
566
return !$ child ->value instanceof ReturnTagValueNode;
493
567
})));
494
568
if (count ($ newPhpDocNodeWithoutReturn ->children ) === 0 ) {
495
- $ new ->setAttribute ('comments ' , []);
569
+ $ old ->setAttribute ('comments ' , []);
496
570
} else {
497
- $ new ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithoutReturn ));
571
+ $ old ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithoutReturn ));
498
572
}
499
573
}
500
574
}
501
575
}
502
576
503
- return [$ new ];
577
+ return [$ old ];
504
578
}
505
579
506
580
/**
@@ -654,13 +728,26 @@ public function __construct(int $phpVersionId)
654
728
{
655
729
$classes = %s;
656
730
$functions = %s;
657
- %s
731
+ // UPDATE BELONGS HERE
658
732
$this->classes = $classes;
659
733
$this->functions = $functions;
660
734
}
661
735
662
736
}
663
737
PHP;
738
+
739
+ if ($ updateTo === null ) {
740
+ file_put_contents (
741
+ __DIR__ . '/../Php8StubsMap.php ' ,
742
+ sprintf (
743
+ $ template ,
744
+ var_export ($ classes , true ),
745
+ var_export ($ functions , true ),
746
+ ),
747
+ );
748
+ return ;
749
+ }
750
+
664
751
$ updateTemplate = <<<'PHP'
665
752
if ($phpVersionId >= %d) {
666
753
$classes = \array_merge($classes, %s);
@@ -670,25 +757,21 @@ public function __construct(int $phpVersionId)
670
757
// UPDATE BELONGS HERE
671
758
PHP;
672
759
673
- $ phpVersion = null ;
674
- if ($ updateTo !== null ) {
675
- $ parts = explode ('. ' , $ updateTo );
676
- $ phpVersion = (int ) $ parts [0 ] * 10000 + (int ) ($ parts [1 ] ?? 0 ) * 100 + (int ) ($ parts [2 ] ?? 0 );
677
- }
760
+ $ parts = explode ('. ' , $ updateTo );
761
+ $ phpVersion = (int ) $ parts [0 ] * 10000 + (int ) ($ parts [1 ] ?? 0 ) * 100 + (int ) ($ parts [2 ] ?? 0 );
762
+ $ updateString = sprintf (
763
+ $ updateTemplate ,
764
+ $ phpVersion ,
765
+ var_export ($ addClasses , true ),
766
+ var_export ($ addFunctions , true )
767
+ );
768
+
769
+ $ currentMap = file_get_contents (__DIR__ . '/../Php8StubsMap.php ' );
770
+ $ newMap = str_replace ('// UPDATE BELONGS HERE ' , $ updateString , $ currentMap );
678
771
679
772
file_put_contents (
680
773
__DIR__ . '/../Php8StubsMap.php ' ,
681
- sprintf (
682
- $ template ,
683
- var_export ($ classes , true ),
684
- var_export ($ functions , true ),
685
- $ phpVersion === null ? '// UPDATE BELONGS HERE ' : sprintf (
686
- $ updateTemplate ,
687
- $ phpVersion ,
688
- var_export ($ addClasses , true ),
689
- var_export ($ addFunctions , true )
690
- )
691
- ),
774
+ $ newMap ,
692
775
);
693
776
}
694
777
0 commit comments