@@ -61,6 +61,7 @@ protected function configure(): void
6161 {
6262 $ this ->setName ('extract ' );
6363 $ this ->addOption ('update ' , null , InputOption::VALUE_NONE );
64+ $ this ->addArgument ('updateFrom ' , InputArgument::OPTIONAL );
6465 $ this ->addArgument ('updateTo ' , InputArgument::OPTIONAL );
6566 }
6667
@@ -72,10 +73,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7273 throw new \LogicException ('Invalid stubs path ' );
7374 }
7475
76+ $ updateFrom = $ input ->getArgument ('updateFrom ' );
7577 $ updateTo = $ input ->getArgument ('updateTo ' );
7678 if (!$ isUpdate ) {
7779 $ this ->clearOldStubs ($ ourStubsDir );
7880 } else {
81+ if ($ updateFrom === null ) {
82+ throw new \LogicException ('Missing arguments ' );
83+ }
7984 if ($ updateTo === null ) {
8085 throw new \LogicException ('Missing arguments ' );
8186 }
@@ -92,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9297 $ addFunctions = [];
9398 foreach ($ finder as $ file ) {
9499 $ stubPath = $ file ->getRealPath ();
95- [$ tmpClasses , $ tmpFunctions ] = $ this ->extractStub ($ stubPath , $ file ->getRelativePathname (), $ isUpdate , $ updateTo );
100+ [$ tmpClasses , $ tmpFunctions ] = $ this ->extractStub ($ stubPath , $ file ->getRelativePathname (), $ isUpdate , $ updateFrom , $ updateTo );
96101 foreach ($ tmpClasses as $ className => $ fileName ) {
97102 $ addClasses [$ className ] = $ fileName ;
98103 }
@@ -108,7 +113,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
108113 $ addFunctions = [];
109114 } else {
110115 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 ));
112118 $ classes = $ map ->classes ;
113119 $ functions = $ map ->functions ;
114120 foreach ($ addClasses as $ className => $ fileName ) {
@@ -117,7 +123,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
117123 }
118124
119125 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 ;
121128 }
122129
123130 unset($ addClasses [$ className ]);
@@ -128,7 +135,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
128135 }
129136
130137 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 ;
132140 }
133141
134142 unset($ addFunctions [$ functionName ]);
@@ -164,11 +172,9 @@ private function clearOldStubs(string $ourStubsDir): void
164172 }
165173
166174 /**
167- * @param string $stubPath
168- * @param string $relativeStubPath
169175 * @return array{array<string, string>, array<string, string>}
170176 */
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
172178 {
173179 $ nameResolver = new PhpParser \NodeVisitor \NameResolver ;
174180 $ nodeTraverser = new PhpParser \NodeTraverser ;
@@ -179,7 +185,7 @@ private function extractStub(string $stubPath, string $relativeStubPath, bool $i
179185 private string $ stubPath ;
180186
181187 /** @var PhpParser\Node\Stmt[] */
182- private array $ stmts ;
188+ private array $ stmts = [] ;
183189
184190 public function __construct (string $ stubPath )
185191 {
@@ -209,6 +215,10 @@ public function enterNode(Node $node)
209215 // pass
210216 } elseif ($ node instanceof Node \Name) {
211217 // 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 ;
212222 } else {
213223 throw new \Exception (sprintf ('Unhandled node type %s in %s on line %s. ' , get_class ($ node ), $ this ->stubPath , $ node ->getLine ()));
214224 }
@@ -303,7 +313,7 @@ public function clear(): void
303313 $ visitor ->clear ();
304314 $ nodeTraverser ->traverse ($ oldStubAst );
305315
306- $ oldStmts = $ visitor ->getStmts ();
316+ [ $ untouchedStmts , $ oldStmts] = $ this -> filterStatementsByVersion ( $ visitor ->getStmts (), $ updateFrom );
307317 if (count ($ oldStmts ) !== 1 ) {
308318 throw new \LogicException ('There is supposed to be one statement in the old AST: ' . $ targetStubPath );
309319 }
@@ -317,24 +327,85 @@ public function clear(): void
317327 $ oldStmt = new Node \Stmt \Namespace_ ($ oldStmt ->namespacedName ->slice (0 , -1 ), [$ oldStmt ]);
318328 }
319329
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) ));
322332 }
323333
324334 return [$ classes , $ functions ];
325335 }
326336
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+
327398 /** @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
329400 {
330401 if ($ old instanceof Node \Stmt \Namespace_ && $ new instanceof Node \Stmt \Namespace_) {
331402 if ($ old ->name ->toString () !== $ new ->name ->toString ()) {
332403 throw new \LogicException ('Namespace name changed ' );
333404 }
334405
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 ))];
336407 } 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 );
338409 }
339410
340411 throw new \LogicException ('Something about a namespace changed ' );
@@ -345,8 +416,9 @@ private function compareStatements(Node\Stmt $old, Node\Stmt $new, string $updat
345416 * @param Node\Stmt[] $newStmts
346417 * @return Node\Stmt[]
347418 */
348- private function compareStatementsInNamespace (array $ oldStmts , array $ newStmts , string $ updateTo ): array
419+ private function compareStatementsInNamespace (array $ oldStmts , array $ newStmts , string $ updateFrom , string $ updateTo ): array
349420 {
421+ [$ untouchedStmts , $ oldStmts ] = $ this ->filterStatementsByVersion ($ oldStmts , $ updateFrom );
350422 if (count ($ oldStmts ) !== 1 || count ($ newStmts ) !== 1 ) {
351423 throw new \LogicException ('There is supposed to be one statement in the AST ' );
352424 }
@@ -358,27 +430,29 @@ private function compareStatementsInNamespace(array $oldStmts, array $newStmts,
358430 if ($ old ->namespacedName ->toString () !== $ new ->namespacedName ->toString ()) {
359431 throw new \LogicException ('Classname changed ' );
360432 }
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 );
362435 $ oldMethods = [];
363- foreach ($ old -> stmts as $ stmt ) {
436+ foreach ($ oldMethodsStmt as $ stmt ) {
364437 if (!$ stmt instanceof Node \Stmt \ClassMethod) {
365438 continue ;
366439 }
367440
368441 $ oldMethods [$ stmt ->name ->toLowerString ()] = $ stmt ;
369442 }
370443
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 ) {
374447 $ methodName = $ stmt ->name ->toLowerString ();
375448 if (!array_key_exists ($ methodName , $ oldMethods )) {
376- $ new -> stmts [ $ i ] ->attrGroups [] = new Node \AttributeGroup ([
449+ $ stmt ->attrGroups [] = new Node \AttributeGroup ([
377450 new Node \Attribute (
378451 new Node \Name \FullyQualified ('Since ' ),
379452 [new Node \Arg (new Node \Scalar \String_ ($ updateTo ))],
380453 ),
381454 ]);
455+ $ newStmtsToSet [] = $ stmt ;
382456 continue ;
383457 }
384458
@@ -389,10 +463,10 @@ private function compareStatementsInNamespace(array $oldStmts, array $newStmts,
389463
390464 // todo has a method been removed?
391465
392- $ new ->stmts = $ newStmtsToSet ;
466+ $ old ->stmts = $ newStmtsToSet ;
393467 }
394468
395- return [$ new ];
469+ return [$ old ];
396470 }
397471
398472 if ($ old instanceof Node \Stmt \Function_ && $ new instanceof Node \Stmt \Function_) {
@@ -416,14 +490,14 @@ private function compareFunctions(Node\FunctionLike $old, Node\FunctionLike $new
416490 if ($ old ->getDocComment () !== null ) {
417491 $ oldPhpDocNode = $ this ->parseDocComment ($ old ->getDocComment ()->getText ());
418492 $ oldPhpDocReturn = $ this ->findPhpDocReturn ($ oldPhpDocNode );
419- if ($ oldPhpDocNode !== null ) {
493+ if ($ oldPhpDocReturn !== null ) {
420494 $ newPhpDocNode = $ this ->parseDocComment ($ new ->getDocComment ()->getText ());
421495 $ newPhpDocReturn = $ this ->findPhpDocReturn ($ newPhpDocNode );
422496 if ($ newPhpDocReturn === null ) {
423497 $ children = $ newPhpDocNode ->children ;
424498 $ children [] = new PhpDocTagNode ('@return ' , $ oldPhpDocReturn );
425499 $ newPhpDocNodeWithReturn = new PhpDocNode ($ children );
426- $ new ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithReturn ));
500+ $ old ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithReturn ));
427501 }
428502 }
429503 }
@@ -492,15 +566,15 @@ private function compareFunctions(Node\FunctionLike $old, Node\FunctionLike $new
492566 return !$ child ->value instanceof ReturnTagValueNode;
493567 })));
494568 if (count ($ newPhpDocNodeWithoutReturn ->children ) === 0 ) {
495- $ new ->setAttribute ('comments ' , []);
569+ $ old ->setAttribute ('comments ' , []);
496570 } else {
497- $ new ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithoutReturn ));
571+ $ old ->setDocComment (new Comment \Doc ((string ) $ newPhpDocNodeWithoutReturn ));
498572 }
499573 }
500574 }
501575 }
502576
503- return [$ new ];
577+ return [$ old ];
504578 }
505579
506580 /**
@@ -654,13 +728,26 @@ public function __construct(int $phpVersionId)
654728 {
655729 $classes = %s;
656730 $functions = %s;
657- %s
731+ // UPDATE BELONGS HERE
658732 $this->classes = $classes;
659733 $this->functions = $functions;
660734 }
661735
662736}
663737PHP;
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+
664751 $ updateTemplate = <<<'PHP'
665752if ($phpVersionId >= %d) {
666753 $classes = \array_merge($classes, %s);
@@ -670,25 +757,21 @@ public function __construct(int $phpVersionId)
670757// UPDATE BELONGS HERE
671758PHP;
672759
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 );
678771
679772 file_put_contents (
680773 __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 ,
692775 );
693776 }
694777
0 commit comments