Skip to content

Commit 6055fa9

Browse files
committed
Extract PHP 8.2 stubs
1 parent 1d651bc commit 6055fa9

File tree

2 files changed

+143
-46
lines changed

2 files changed

+143
-46
lines changed

.github/workflows/update.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,21 @@ jobs:
4848
path: "php-src"
4949
ref: "PHP-8.1"
5050
- name: "Update stubs"
51-
run: "./extractor/extract.php --update -- 8.1"
51+
run: "./extractor/extract.php --update -- 8.0 8.1"
52+
53+
# ---
54+
55+
- name: "Delete checked out php-src repo"
56+
run: "rm -rf php-src"
57+
- name: "Checkout PHP 8.2"
58+
uses: actions/checkout@v3
59+
with:
60+
repository: "php/php-src"
61+
path: "php-src"
62+
ref: "master"
63+
- name: "Update stubs"
64+
run: "./extractor/extract.php --update -- 8.1 8.2"
65+
5266
# end repeat
5367

5468
- name: 'Get previous tag'

extractor/extract.php

Lines changed: 128 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
663737
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+
664751
$updateTemplate = <<<'PHP'
665752
if ($phpVersionId >= %d) {
666753
$classes = \array_merge($classes, %s);
@@ -670,25 +757,21 @@ public function __construct(int $phpVersionId)
670757
// UPDATE BELONGS HERE
671758
PHP;
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

Comments
 (0)