From e5ebd326edd571b48e1dd01c3e00bc3fd0a6966c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Wed, 13 Mar 2024 23:24:37 +0100 Subject: [PATCH] Improve BC support of arginfo files fenerated by gen_stub.php - Declared compatibility expectations of stub files are now enforced by a ZEND_STATIC_ASSERT calll 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... --- build/gen_stub.php | 144 ++++++++++++++++++++++++++++------- ext/zend_test/test_arginfo.h | 3 + 2 files changed, 119 insertions(+), 28 deletions(-) diff --git a/build/gen_stub.php b/build/gen_stub.php index 4715f7a401310..e5645f233f371 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -122,17 +122,19 @@ function processStubFile(string $stubFile, Context $context, bool $includeOnly = echo "Saved $arginfoFile\n"; } - if ($fileInfo->generateLegacyArginfoForPhpVersionId !== null && $fileInfo->generateLegacyArginfoForPhpVersionId < PHP_80_VERSION_ID) { + if ($fileInfo->shouldGenerateLegacyArginfo()) { $legacyFileInfo = clone $fileInfo; + $legacyFileInfo->legacyArginfoGeneration = true; + $phpVersionIdMinimumCompatibility = $legacyFileInfo->getMinimumPhpVersionIdCompatibility(); foreach ($legacyFileInfo->getAllFuncInfos() as $funcInfo) { - $funcInfo->discardInfoForOldPhpVersions(); + $funcInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); } - foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) { - $constInfo->discardInfoForOldPhpVersions(); + foreach ($legacyFileInfo->getAllClassInfos() as $classInfo) { + $classInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); } - foreach ($legacyFileInfo->getAllPropertyInfos() as $propertyInfo) { - $propertyInfo->discardInfoForOldPhpVersions(); + foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) { + $constInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); } $arginfoCode = generateArgInfoCode( @@ -1532,14 +1534,18 @@ public function getOptimizerInfo(): ?string { return "\tF" . $this->return->refcount . '("' . addslashes($this->name->__toString()) . '", ' . $type->toOptimizerTypeMask() . "),\n"; } - public function discardInfoForOldPhpVersions(): void { + public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void { $this->attributes = []; $this->return->type = null; + $this->framelessFunctionInfos = []; + $this->exposedDocComment = null; + $this->supportsCompileTimeEval = false; foreach ($this->args as $arg) { $arg->type = null; $arg->defaultValue = null; $arg->attributes = []; } + $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility; } /** @return array */ @@ -2127,6 +2133,15 @@ public function __clone() $this->args[$key] = clone $argInfo; } $this->return = clone $this->return; + foreach ($this->attributes as $key => $attribute) { + $this->attributes[$key] = clone $attribute; + } + foreach ($this->framelessFunctionInfos as $key => $framelessFunctionInfo) { + $this->framelessFunctionInfos[$key] = clone $framelessFunctionInfo; + } + if ($this->exposedDocComment) { + $this->exposedDocComment = clone $this->exposedDocComment; + } } } @@ -2355,7 +2370,7 @@ abstract protected function getFieldSynopsisName(): string; /** @param array $allConstInfos */ abstract protected function getFieldSynopsisValueString(array $allConstInfos): ?string; - abstract public function discardInfoForOldPhpVersions(): void; + abstract public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void; protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void { @@ -2628,17 +2643,19 @@ public function getPredefinedConstantEntry(DOMDocument $doc, int $indentationLev return $entryElement; } - public function discardInfoForOldPhpVersions(): void { + public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { $this->type = null; $this->flags &= ~Modifiers::FINAL; $this->isDeprecated = false; $this->attributes = []; + $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; } /** @param array $allConstInfos */ public function getDeclaration(array $allConstInfos): string { - $simpleType = ($this->phpDocType ?? $this->type)->tryToSimpleType(); + $type = $this->phpDocType ?? $this->type; + $simpleType = $type ? $type->tryToSimpleType() : null; if ($simpleType && $simpleType->name === "mixed") { $simpleType = null; } @@ -2909,10 +2926,11 @@ protected function getFieldSynopsisValueString(array $allConstInfos): ?string return $this->defaultValueString; } - public function discardInfoForOldPhpVersions(): void { + public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { $this->type = null; $this->flags &= ~Modifiers::READONLY; $this->attributes = []; + $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; } /** @param array $allConstInfos */ @@ -2940,7 +2958,6 @@ public function getDeclaration(array $allConstInfos): string { $code .= "\tzend_string *property_{$propertyName}_name = zend_string_init(\"$propertyName\", sizeof(\"$propertyName\") - 1, 1);\n"; $nameCode = "property_{$propertyName}_name"; - $typeCode = $this->getTypeCode($propertyName, $code); if ($this->exposedDocComment) { $commentCode = "property_{$propertyName}_comment"; @@ -2956,7 +2973,14 @@ public function getDeclaration(array $allConstInfos): string { } else { $template = "\t"; } - $template .= "zend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, $commentCode, $typeCode);\n"; + + if ($this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_80_VERSION_ID) { + $typeCode = $this->getTypeCode($propertyName, $code); + $template .= "zend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, $commentCode, $typeCode);\n"; + } else { + $template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n"; + } + $flagsCode = generateVersionDependentFlagCode( $template, $this->getFlagsByPhpVersion(), @@ -3007,6 +3031,12 @@ public function __clone() if ($this->type) { $this->type = clone $this->type; } + foreach ($this->attributes as $key => $attribute) { + $this->attributes[$key] = clone $attribute; + } + if ($this->exposedDocComment) { + $this->exposedDocComment = clone $this->exposedDocComment; + } } } @@ -3383,6 +3413,19 @@ private function getFlagsByPhpVersion(): array ]; } + public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { + $this->attributes = []; + $this->flags &= ~Modifiers::READONLY; + $this->exposedDocComment = null; + $this->isStrictProperties = false; + $this->isNotSerializable = false; + + foreach ($this->propertyInfos as $propertyInfo) { + $propertyInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); + } + $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; + } + /** * @param array $classMap * @param array $allConstInfos @@ -3801,6 +3844,10 @@ private function createIncludeElement(DOMDocument $doc, string $query): DOMEleme public function __clone() { + foreach ($this->constInfos as $key => $constInfo) { + $this->constInfos[$key] = clone $constInfo; + } + foreach ($this->propertyInfos as $key => $propertyInfo) { $this->propertyInfos[$key] = clone $propertyInfo; } @@ -3808,6 +3855,14 @@ public function __clone() foreach ($this->funcInfos as $key => $funcInfo) { $this->funcInfos[$key] = clone $funcInfo; } + + foreach ($this->attributes as $key => $attribute) { + $this->attributes[$key] = clone $attribute; + } + + if ($this->exposedDocComment) { + $this->exposedDocComment = clone $this->exposedDocComment; + } } /** @@ -3848,9 +3903,10 @@ class FileInfo { public array $classInfos = []; public bool $generateFunctionEntries = false; public string $declarationPrefix = ""; - public ?int $generateLegacyArginfoForPhpVersionId = null; public bool $generateClassEntries = false; public bool $isUndocumentable = false; + public bool $legacyArginfoGeneration = false; + private ?int $minimumPhpVersionIdCompatibility = null; /** * @return iterable @@ -3880,16 +3936,20 @@ public function getAllConstInfos(): array { } /** - * @return iterable + * @return iterable */ - public function getAllPropertyInfos(): iterable { + public function getAllClassInfos(): iterable { foreach ($this->classInfos as $classInfo) { - yield from $classInfo->propertyInfos; + yield $classInfo; } } public function __clone() { + foreach ($this->constInfos as $key => $constInfo) { + $this->constInfos[$key] = clone $constInfo; + } + foreach ($this->funcInfos as $key => $funcInfo) { $this->funcInfos[$key] = clone $funcInfo; } @@ -3898,6 +3958,26 @@ public function __clone() $this->classInfos[$key] = clone $classInfo; } } + + public function setMinimumPhpVersionIdCompatibility(?int $minimumPhpVersionIdCompatibility) { + $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility; + } + + public function getMinimumPhpVersionIdCompatibility(): ?int { + // Non-legacy arginfo files are always PHP 8.0+ compatible + if (!$this->legacyArginfoGeneration && + $this->minimumPhpVersionIdCompatibility !== null && + $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID + ) { + return PHP_80_VERSION_ID; + } + + return $this->minimumPhpVersionIdCompatibility; + } + + public function shouldGenerateLegacyArginfo(): bool { + return $this->minimumPhpVersionIdCompatibility !== null && $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID; + } } class DocCommentTag { @@ -4541,7 +4621,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $stmt->getComments(), $cond, $fileInfo->isUndocumentable, - $fileInfo->generateLegacyArginfoForPhpVersionId, + $fileInfo->getMinimumPhpVersionIdCompatibility(), [] ); } @@ -4557,7 +4637,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $stmt, $cond, $fileInfo->isUndocumentable, - $fileInfo->generateLegacyArginfoForPhpVersionId + $fileInfo->getMinimumPhpVersionIdCompatibility() ); continue; } @@ -4588,7 +4668,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $classStmt->getComments(), $cond, $fileInfo->isUndocumentable, - $fileInfo->generateLegacyArginfoForPhpVersionId, + $fileInfo->getMinimumPhpVersionIdCompatibility(), createAttributes($classStmt->attrGroups) ); } @@ -4604,7 +4684,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $classStmt->type, $classStmt->getComments(), $prettyPrinter, - $fileInfo->generateLegacyArginfoForPhpVersionId, + $fileInfo->getMinimumPhpVersionIdCompatibility(), createAttributes($classStmt->attrGroups) ); } @@ -4620,7 +4700,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $classStmt, $cond, $fileInfo->isUndocumentable, - $fileInfo->generateLegacyArginfoForPhpVersionId + $fileInfo->getMinimumPhpVersionIdCompatibility() ); } else if ($classStmt instanceof Stmt\EnumCase) { $enumCaseInfos[] = new EnumCaseInfo( @@ -4631,7 +4711,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac } $fileInfo->classInfos[] = parseClass( - $className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->generateLegacyArginfoForPhpVersionId, $fileInfo->isUndocumentable + $className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->getMinimumPhpVersionIdCompatibility(), $fileInfo->isUndocumentable ); continue; } @@ -4678,11 +4758,11 @@ protected function pName_FullyQualified(Name\FullyQualified $node): string { throw new Exception( "Legacy PHP version must be one of: \"" . PHP_70_VERSION_ID . "\" (PHP 7.0), \"" . PHP_80_VERSION_ID . "\" (PHP 8.0), " . "\"" . PHP_81_VERSION_ID . "\" (PHP 8.1), \"" . PHP_82_VERSION_ID . "\" (PHP 8.2), \"" . PHP_83_VERSION_ID . "\" (PHP 8.3), " . - "\"" . $tag->value . "\" provided" + "\"" . PHP_84_VERSION_ID . "\" (PHP 8.4), \"" . $tag->value . "\" provided" ); } - $fileInfo->generateLegacyArginfoForPhpVersionId = $tag->value ? (int) $tag->value : PHP_70_VERSION_ID; + $fileInfo->setMinimumPhpVersionIdCompatibility($tag->value ? (int) $tag->value : PHP_70_VERSION_ID); } else if ($tag->name === 'generate-class-entries') { $fileInfo->generateClassEntries = true; $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; @@ -4705,7 +4785,7 @@ function funcInfoToCode(FileInfo $fileInfo, FuncInfo $funcInfo): string { $code = ''; $returnType = $funcInfo->return->type; $isTentativeReturnType = $funcInfo->return->tentativeReturnType; - $php81MinimumCompatibility = $fileInfo->generateLegacyArginfoForPhpVersionId === null || $fileInfo->generateLegacyArginfoForPhpVersionId >= PHP_81_VERSION_ID; + $php81MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_81_VERSION_ID; if ($returnType !== null) { if ($isTentativeReturnType && !$php81MinimumCompatibility) { @@ -4864,6 +4944,14 @@ function generateArgInfoCode( $code = "/* This is a generated file, edit the .stub.php file instead.\n" . " * Stub hash: $stubHash */\n"; + $minimumPhpVersionIdCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility(); + if ($minimumPhpVersionIdCompatibility !== null) { + $code .= "\nZEND_STATIC_ASSERT(PHP_VERSION_ID >= $minimumPhpVersionIdCompatibility, "; + $code .= "\"{$stubFilenameWithoutExtension}_arginfo.h only supports "; + $code .= "PHP version ID $minimumPhpVersionIdCompatibility or newer, \"\n"; + $code .= "\t\"but it is included on an older PHP version\");\n"; + } + $generatedFuncInfos = []; $argInfoCode = generateCodeWithConditions( @@ -4924,10 +5012,10 @@ static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarat } } - $php80MinimumCompatibility = $fileInfo->generateLegacyArginfoForPhpVersionId === null || $fileInfo->generateLegacyArginfoForPhpVersionId >= PHP_80_VERSION_ID; + $php80MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_80_VERSION_ID; if ($fileInfo->generateClassEntries) { - if ($attributeInitializationCode = generateFunctionAttributeInitialization($fileInfo->funcInfos, $allConstInfos, $fileInfo->generateLegacyArginfoForPhpVersionId, null)) { + if ($attributeInitializationCode = generateFunctionAttributeInitialization($fileInfo->funcInfos, $allConstInfos, $fileInfo->getMinimumPhpVersionIdCompatibility(), null)) { if (!$php80MinimumCompatibility) { $attributeInitializationCode = "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")" . $attributeInitializationCode . "#endif\n"; } diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 736dcfd8b2549..1f846e830525b 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,6 +1,9 @@ /* This is a generated file, edit the .stub.php file instead. * Stub hash: dd3f852ea9f8e3a356ed226380edf5cc336f8a4e */ +ZEND_STATIC_ASSERT(PHP_VERSION_ID >= 80000, "test_arginfo.h only supports PHP version ID 80000 or newer, " + "but it is included on an older PHP version"); + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO()