Skip to content

Commit 5992a29

Browse files
authored
Improve BC support of arginfo files fenerated by gen_stub.php (#13705)
- Declared compatibility expectations of stub files are now enforced by a ZEND_STATIC_ASSERT call at the top of arginfo files - Property registration for PHP 7 is fixed: function zend_declare_property_ex() is used again instead of zend_declare_typed_property(). This has been a regression since I added support for exposing doc comments. - As a defensive measure, deep cloning is performed before newer features (type declarations, attributes etc.) are discarded before generating legacy arginfo files. Until now, some of the objects were forgotten to be taken care of. These omissions may have resulted in some weird bugs in theory (but probably they didn't have much impact in practice). - PHP version related conditions inside *non-legacy arginfo files* used to possibly check for the 70000 version iD until now if compatibility with PHP 7.0 was declared in a stub. This was not 100% correct, since non-legacy arginfo files are only for PHP 8.0+. Now, I made sure that at least PHP version ID 80000 is used in the preprocessor conditions. The solution was a bit tricky though...
1 parent 4e934d7 commit 5992a29

File tree

2 files changed

+119
-28
lines changed

2 files changed

+119
-28
lines changed

build/gen_stub.php

+116-28
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,19 @@ function processStubFile(string $stubFile, Context $context, bool $includeOnly =
122122
echo "Saved $arginfoFile\n";
123123
}
124124

125-
if ($fileInfo->generateLegacyArginfoForPhpVersionId !== null && $fileInfo->generateLegacyArginfoForPhpVersionId < PHP_80_VERSION_ID) {
125+
if ($fileInfo->shouldGenerateLegacyArginfo()) {
126126
$legacyFileInfo = clone $fileInfo;
127+
$legacyFileInfo->legacyArginfoGeneration = true;
128+
$phpVersionIdMinimumCompatibility = $legacyFileInfo->getMinimumPhpVersionIdCompatibility();
127129

128130
foreach ($legacyFileInfo->getAllFuncInfos() as $funcInfo) {
129-
$funcInfo->discardInfoForOldPhpVersions();
131+
$funcInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
130132
}
131-
foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) {
132-
$constInfo->discardInfoForOldPhpVersions();
133+
foreach ($legacyFileInfo->getAllClassInfos() as $classInfo) {
134+
$classInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
133135
}
134-
foreach ($legacyFileInfo->getAllPropertyInfos() as $propertyInfo) {
135-
$propertyInfo->discardInfoForOldPhpVersions();
136+
foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) {
137+
$constInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
136138
}
137139

138140
$arginfoCode = generateArgInfoCode(
@@ -1532,14 +1534,18 @@ public function getOptimizerInfo(): ?string {
15321534
return "\tF" . $this->return->refcount . '("' . addslashes($this->name->__toString()) . '", ' . $type->toOptimizerTypeMask() . "),\n";
15331535
}
15341536

1535-
public function discardInfoForOldPhpVersions(): void {
1537+
public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void {
15361538
$this->attributes = [];
15371539
$this->return->type = null;
1540+
$this->framelessFunctionInfos = [];
1541+
$this->exposedDocComment = null;
1542+
$this->supportsCompileTimeEval = false;
15381543
foreach ($this->args as $arg) {
15391544
$arg->type = null;
15401545
$arg->defaultValue = null;
15411546
$arg->attributes = [];
15421547
}
1548+
$this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility;
15431549
}
15441550

15451551
/** @return array<int, string[]> */
@@ -2127,6 +2133,15 @@ public function __clone()
21272133
$this->args[$key] = clone $argInfo;
21282134
}
21292135
$this->return = clone $this->return;
2136+
foreach ($this->attributes as $key => $attribute) {
2137+
$this->attributes[$key] = clone $attribute;
2138+
}
2139+
foreach ($this->framelessFunctionInfos as $key => $framelessFunctionInfo) {
2140+
$this->framelessFunctionInfos[$key] = clone $framelessFunctionInfo;
2141+
}
2142+
if ($this->exposedDocComment) {
2143+
$this->exposedDocComment = clone $this->exposedDocComment;
2144+
}
21302145
}
21312146
}
21322147

@@ -2355,7 +2370,7 @@ abstract protected function getFieldSynopsisName(): string;
23552370
/** @param array<string, ConstInfo> $allConstInfos */
23562371
abstract protected function getFieldSynopsisValueString(array $allConstInfos): ?string;
23572372

2358-
abstract public function discardInfoForOldPhpVersions(): void;
2373+
abstract public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void;
23592374

23602375
protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void
23612376
{
@@ -2628,17 +2643,19 @@ public function getPredefinedConstantEntry(DOMDocument $doc, int $indentationLev
26282643
return $entryElement;
26292644
}
26302645

2631-
public function discardInfoForOldPhpVersions(): void {
2646+
public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void {
26322647
$this->type = null;
26332648
$this->flags &= ~Modifiers::FINAL;
26342649
$this->isDeprecated = false;
26352650
$this->attributes = [];
2651+
$this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
26362652
}
26372653

26382654
/** @param array<string, ConstInfo> $allConstInfos */
26392655
public function getDeclaration(array $allConstInfos): string
26402656
{
2641-
$simpleType = ($this->phpDocType ?? $this->type)->tryToSimpleType();
2657+
$type = $this->phpDocType ?? $this->type;
2658+
$simpleType = $type ? $type->tryToSimpleType() : null;
26422659
if ($simpleType && $simpleType->name === "mixed") {
26432660
$simpleType = null;
26442661
}
@@ -2909,10 +2926,11 @@ protected function getFieldSynopsisValueString(array $allConstInfos): ?string
29092926
return $this->defaultValueString;
29102927
}
29112928

2912-
public function discardInfoForOldPhpVersions(): void {
2929+
public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void {
29132930
$this->type = null;
29142931
$this->flags &= ~Modifiers::READONLY;
29152932
$this->attributes = [];
2933+
$this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
29162934
}
29172935

29182936
/** @param array<string, ConstInfo> $allConstInfos */
@@ -2940,7 +2958,6 @@ public function getDeclaration(array $allConstInfos): string {
29402958

29412959
$code .= "\tzend_string *property_{$propertyName}_name = zend_string_init(\"$propertyName\", sizeof(\"$propertyName\") - 1, 1);\n";
29422960
$nameCode = "property_{$propertyName}_name";
2943-
$typeCode = $this->getTypeCode($propertyName, $code);
29442961

29452962
if ($this->exposedDocComment) {
29462963
$commentCode = "property_{$propertyName}_comment";
@@ -2956,7 +2973,14 @@ public function getDeclaration(array $allConstInfos): string {
29562973
} else {
29572974
$template = "\t";
29582975
}
2959-
$template .= "zend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, $commentCode, $typeCode);\n";
2976+
2977+
if ($this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_80_VERSION_ID) {
2978+
$typeCode = $this->getTypeCode($propertyName, $code);
2979+
$template .= "zend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, $commentCode, $typeCode);\n";
2980+
} else {
2981+
$template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n";
2982+
}
2983+
29602984
$flagsCode = generateVersionDependentFlagCode(
29612985
$template,
29622986
$this->getFlagsByPhpVersion(),
@@ -3007,6 +3031,12 @@ public function __clone()
30073031
if ($this->type) {
30083032
$this->type = clone $this->type;
30093033
}
3034+
foreach ($this->attributes as $key => $attribute) {
3035+
$this->attributes[$key] = clone $attribute;
3036+
}
3037+
if ($this->exposedDocComment) {
3038+
$this->exposedDocComment = clone $this->exposedDocComment;
3039+
}
30103040
}
30113041
}
30123042

@@ -3383,6 +3413,19 @@ private function getFlagsByPhpVersion(): array
33833413
];
33843414
}
33853415

3416+
public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void {
3417+
$this->attributes = [];
3418+
$this->flags &= ~Modifiers::READONLY;
3419+
$this->exposedDocComment = null;
3420+
$this->isStrictProperties = false;
3421+
$this->isNotSerializable = false;
3422+
3423+
foreach ($this->propertyInfos as $propertyInfo) {
3424+
$propertyInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
3425+
}
3426+
$this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
3427+
}
3428+
33863429
/**
33873430
* @param array<string, ClassInfo> $classMap
33883431
* @param array<string, ConstInfo> $allConstInfos
@@ -3801,13 +3844,25 @@ private function createIncludeElement(DOMDocument $doc, string $query): DOMEleme
38013844

38023845
public function __clone()
38033846
{
3847+
foreach ($this->constInfos as $key => $constInfo) {
3848+
$this->constInfos[$key] = clone $constInfo;
3849+
}
3850+
38043851
foreach ($this->propertyInfos as $key => $propertyInfo) {
38053852
$this->propertyInfos[$key] = clone $propertyInfo;
38063853
}
38073854

38083855
foreach ($this->funcInfos as $key => $funcInfo) {
38093856
$this->funcInfos[$key] = clone $funcInfo;
38103857
}
3858+
3859+
foreach ($this->attributes as $key => $attribute) {
3860+
$this->attributes[$key] = clone $attribute;
3861+
}
3862+
3863+
if ($this->exposedDocComment) {
3864+
$this->exposedDocComment = clone $this->exposedDocComment;
3865+
}
38113866
}
38123867

38133868
/**
@@ -3848,9 +3903,10 @@ class FileInfo {
38483903
public array $classInfos = [];
38493904
public bool $generateFunctionEntries = false;
38503905
public string $declarationPrefix = "";
3851-
public ?int $generateLegacyArginfoForPhpVersionId = null;
38523906
public bool $generateClassEntries = false;
38533907
public bool $isUndocumentable = false;
3908+
public bool $legacyArginfoGeneration = false;
3909+
private ?int $minimumPhpVersionIdCompatibility = null;
38543910

38553911
/**
38563912
* @return iterable<FuncInfo>
@@ -3880,16 +3936,20 @@ public function getAllConstInfos(): array {
38803936
}
38813937

38823938
/**
3883-
* @return iterable<PropertyInfo>
3939+
* @return iterable<ClassInfo>
38843940
*/
3885-
public function getAllPropertyInfos(): iterable {
3941+
public function getAllClassInfos(): iterable {
38863942
foreach ($this->classInfos as $classInfo) {
3887-
yield from $classInfo->propertyInfos;
3943+
yield $classInfo;
38883944
}
38893945
}
38903946

38913947
public function __clone()
38923948
{
3949+
foreach ($this->constInfos as $key => $constInfo) {
3950+
$this->constInfos[$key] = clone $constInfo;
3951+
}
3952+
38933953
foreach ($this->funcInfos as $key => $funcInfo) {
38943954
$this->funcInfos[$key] = clone $funcInfo;
38953955
}
@@ -3898,6 +3958,26 @@ public function __clone()
38983958
$this->classInfos[$key] = clone $classInfo;
38993959
}
39003960
}
3961+
3962+
public function setMinimumPhpVersionIdCompatibility(?int $minimumPhpVersionIdCompatibility) {
3963+
$this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility;
3964+
}
3965+
3966+
public function getMinimumPhpVersionIdCompatibility(): ?int {
3967+
// Non-legacy arginfo files are always PHP 8.0+ compatible
3968+
if (!$this->legacyArginfoGeneration &&
3969+
$this->minimumPhpVersionIdCompatibility !== null &&
3970+
$this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID
3971+
) {
3972+
return PHP_80_VERSION_ID;
3973+
}
3974+
3975+
return $this->minimumPhpVersionIdCompatibility;
3976+
}
3977+
3978+
public function shouldGenerateLegacyArginfo(): bool {
3979+
return $this->minimumPhpVersionIdCompatibility !== null && $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID;
3980+
}
39013981
}
39023982

39033983
class DocCommentTag {
@@ -4541,7 +4621,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac
45414621
$stmt->getComments(),
45424622
$cond,
45434623
$fileInfo->isUndocumentable,
4544-
$fileInfo->generateLegacyArginfoForPhpVersionId,
4624+
$fileInfo->getMinimumPhpVersionIdCompatibility(),
45454625
[]
45464626
);
45474627
}
@@ -4557,7 +4637,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac
45574637
$stmt,
45584638
$cond,
45594639
$fileInfo->isUndocumentable,
4560-
$fileInfo->generateLegacyArginfoForPhpVersionId
4640+
$fileInfo->getMinimumPhpVersionIdCompatibility()
45614641
);
45624642
continue;
45634643
}
@@ -4588,7 +4668,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac
45884668
$classStmt->getComments(),
45894669
$cond,
45904670
$fileInfo->isUndocumentable,
4591-
$fileInfo->generateLegacyArginfoForPhpVersionId,
4671+
$fileInfo->getMinimumPhpVersionIdCompatibility(),
45924672
createAttributes($classStmt->attrGroups)
45934673
);
45944674
}
@@ -4604,7 +4684,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac
46044684
$classStmt->type,
46054685
$classStmt->getComments(),
46064686
$prettyPrinter,
4607-
$fileInfo->generateLegacyArginfoForPhpVersionId,
4687+
$fileInfo->getMinimumPhpVersionIdCompatibility(),
46084688
createAttributes($classStmt->attrGroups)
46094689
);
46104690
}
@@ -4620,7 +4700,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac
46204700
$classStmt,
46214701
$cond,
46224702
$fileInfo->isUndocumentable,
4623-
$fileInfo->generateLegacyArginfoForPhpVersionId
4703+
$fileInfo->getMinimumPhpVersionIdCompatibility()
46244704
);
46254705
} else if ($classStmt instanceof Stmt\EnumCase) {
46264706
$enumCaseInfos[] = new EnumCaseInfo(
@@ -4631,7 +4711,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac
46314711
}
46324712

46334713
$fileInfo->classInfos[] = parseClass(
4634-
$className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->generateLegacyArginfoForPhpVersionId, $fileInfo->isUndocumentable
4714+
$className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->getMinimumPhpVersionIdCompatibility(), $fileInfo->isUndocumentable
46354715
);
46364716
continue;
46374717
}
@@ -4678,11 +4758,11 @@ protected function pName_FullyQualified(Name\FullyQualified $node): string {
46784758
throw new Exception(
46794759
"Legacy PHP version must be one of: \"" . PHP_70_VERSION_ID . "\" (PHP 7.0), \"" . PHP_80_VERSION_ID . "\" (PHP 8.0), " .
46804760
"\"" . PHP_81_VERSION_ID . "\" (PHP 8.1), \"" . PHP_82_VERSION_ID . "\" (PHP 8.2), \"" . PHP_83_VERSION_ID . "\" (PHP 8.3), " .
4681-
"\"" . $tag->value . "\" provided"
4761+
"\"" . PHP_84_VERSION_ID . "\" (PHP 8.4), \"" . $tag->value . "\" provided"
46824762
);
46834763
}
46844764

4685-
$fileInfo->generateLegacyArginfoForPhpVersionId = $tag->value ? (int) $tag->value : PHP_70_VERSION_ID;
4765+
$fileInfo->setMinimumPhpVersionIdCompatibility($tag->value ? (int) $tag->value : PHP_70_VERSION_ID);
46864766
} else if ($tag->name === 'generate-class-entries') {
46874767
$fileInfo->generateClassEntries = true;
46884768
$fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : "";
@@ -4705,7 +4785,7 @@ function funcInfoToCode(FileInfo $fileInfo, FuncInfo $funcInfo): string {
47054785
$code = '';
47064786
$returnType = $funcInfo->return->type;
47074787
$isTentativeReturnType = $funcInfo->return->tentativeReturnType;
4708-
$php81MinimumCompatibility = $fileInfo->generateLegacyArginfoForPhpVersionId === null || $fileInfo->generateLegacyArginfoForPhpVersionId >= PHP_81_VERSION_ID;
4788+
$php81MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_81_VERSION_ID;
47094789

47104790
if ($returnType !== null) {
47114791
if ($isTentativeReturnType && !$php81MinimumCompatibility) {
@@ -4864,6 +4944,14 @@ function generateArgInfoCode(
48644944
$code = "/* This is a generated file, edit the .stub.php file instead.\n"
48654945
. " * Stub hash: $stubHash */\n";
48664946

4947+
$minimumPhpVersionIdCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility();
4948+
if ($minimumPhpVersionIdCompatibility !== null) {
4949+
$code .= "\nZEND_STATIC_ASSERT(PHP_VERSION_ID >= $minimumPhpVersionIdCompatibility, ";
4950+
$code .= "\"{$stubFilenameWithoutExtension}_arginfo.h only supports ";
4951+
$code .= "PHP version ID $minimumPhpVersionIdCompatibility or newer, \"\n";
4952+
$code .= "\t\"but it is included on an older PHP version\");\n";
4953+
}
4954+
48674955
$generatedFuncInfos = [];
48684956

48694957
$argInfoCode = generateCodeWithConditions(
@@ -4924,10 +5012,10 @@ static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarat
49245012
}
49255013
}
49265014

4927-
$php80MinimumCompatibility = $fileInfo->generateLegacyArginfoForPhpVersionId === null || $fileInfo->generateLegacyArginfoForPhpVersionId >= PHP_80_VERSION_ID;
5015+
$php80MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_80_VERSION_ID;
49285016

49295017
if ($fileInfo->generateClassEntries) {
4930-
if ($attributeInitializationCode = generateFunctionAttributeInitialization($fileInfo->funcInfos, $allConstInfos, $fileInfo->generateLegacyArginfoForPhpVersionId, null)) {
5018+
if ($attributeInitializationCode = generateFunctionAttributeInitialization($fileInfo->funcInfos, $allConstInfos, $fileInfo->getMinimumPhpVersionIdCompatibility(), null)) {
49315019
if (!$php80MinimumCompatibility) {
49325020
$attributeInitializationCode = "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")" . $attributeInitializationCode . "#endif\n";
49335021
}

ext/zend_test/test_arginfo.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)