diff options
Diffstat (limited to 'aws/aws-crt-php/gen_stub.php')
-rwxr-xr-x | aws/aws-crt-php/gen_stub.php | 1998 |
1 files changed, 1998 insertions, 0 deletions
diff --git a/aws/aws-crt-php/gen_stub.php b/aws/aws-crt-php/gen_stub.php new file mode 100755 index 00000000..49670326 --- /dev/null +++ b/aws/aws-crt-php/gen_stub.php @@ -0,0 +1,1998 @@ +#!/usr/bin/env php +<?php declare(strict_types=1); + +// This is a copy of the gen_stub.php from the PHP build scripts, modified to +// generate macros that we can abstract across versions of PHP + +use PhpParser\Comment\Doc as DocComment; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\PrettyPrinter\Standard; +use PhpParser\PrettyPrinterAbstract; + +error_reporting(E_ALL); + +/** + * @return FileInfo[] + */ +function processDirectory(string $dir, Context $context): array { + $fileInfos = []; + + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir), + RecursiveIteratorIterator::LEAVES_ONLY + ); + foreach ($it as $file) { + $pathName = $file->getPathName(); + if (preg_match('/\.stub\.php$/', $pathName)) { + $fileInfo = processStubFile($pathName, $context); + if ($fileInfo) { + $fileInfos[] = $fileInfo; + } + } + } + + return $fileInfos; +} + +function processStubFile(string $stubFile, Context $context): ?FileInfo { + try { + if (!file_exists($stubFile)) { + throw new Exception("File $stubFile does not exist"); + } + + $arginfoFile = str_replace('.stub.php', '_arginfo.h', $stubFile); + $legacyFile = str_replace('.stub.php', '_legacy_arginfo.h', $stubFile); + + $stubCode = file_get_contents($stubFile); + $stubHash = computeStubHash($stubCode); + $oldStubHash = extractStubHash($arginfoFile); + if ($stubHash === $oldStubHash && !$context->forceParse) { + /* Stub file did not change, do not regenerate. */ + return null; + } + + initPhpParser(); + $fileInfo = parseStubFile($stubCode); + $arginfoCode = generateArgInfoCode($fileInfo, $stubHash, $context->minimalArgInfo); + if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) { + echo "Saved $arginfoFile\n"; + } + + if ($fileInfo->generateLegacyArginfo) { + foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { + $funcInfo->discardInfoForOldPhpVersions(); + } + $arginfoCode = generateArgInfoCode($fileInfo, $stubHash, $context->minimalArgInfo); + if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) { + echo "Saved $legacyFile\n"; + } + } + + return $fileInfo; + } catch (Exception $e) { + echo "In $stubFile:\n{$e->getMessage()}\n"; + exit(1); + } +} + +function computeStubHash(string $stubCode): string { + return sha1(str_replace("\r\n", "\n", $stubCode)); +} + +function extractStubHash(string $arginfoFile): ?string { + if (!file_exists($arginfoFile)) { + return null; + } + + $arginfoCode = file_get_contents($arginfoFile); + if (!preg_match('/\* Stub hash: ([0-9a-f]+) \*/', $arginfoCode, $matches)) { + return null; + } + + return $matches[1]; +} + +class Context { + /** @var bool */ + public $forceParse = false; + /** @var bool */ + public $forceRegeneration = false; + /** @var bool */ + public $minimalArgInfo = false; +} + +class SimpleType { + /** @var string */ + public $name; + /** @var bool */ + public $isBuiltin; + + public function __construct(string $name, bool $isBuiltin) { + $this->name = $name; + $this->isBuiltin = $isBuiltin; + } + + public static function fromNode(Node $node): SimpleType { + if ($node instanceof Node\Name) { + if ($node->toLowerString() === 'static') { + // PHP internally considers "static" a builtin type. + return new SimpleType($node->toString(), true); + } + + assert($node->isFullyQualified()); + return new SimpleType($node->toString(), false); + } + if ($node instanceof Node\Identifier) { + return new SimpleType($node->toString(), true); + } + throw new Exception("Unexpected node type"); + } + + public static function fromPhpDoc(string $type): SimpleType + { + switch (strtolower($type)) { + case "void": + case "null": + case "false": + case "bool": + case "int": + case "float": + case "string": + case "array": + case "iterable": + case "object": + case "resource": + case "mixed": + case "self": + case "static": + return new SimpleType(strtolower($type), true); + } + + if (strpos($type, "[]") !== false) { + return new SimpleType("array", true); + } + + return new SimpleType($type, false); + } + + public static function null(): SimpleType + { + return new SimpleType("null", true); + } + + public static function void(): SimpleType + { + return new SimpleType("void", true); + } + + public function isNull(): bool { + return $this->isBuiltin && $this->name === 'null'; + } + + public function toTypeCode(): string { + assert($this->isBuiltin); + switch (strtolower($this->name)) { + case "bool": + return "_IS_BOOL"; + case "int": + return "IS_LONG"; + case "float": + return "IS_DOUBLE"; + case "string": + return "IS_STRING"; + case "array": + return "IS_ARRAY"; + case "object": + return "IS_OBJECT"; + case "void": + return "IS_VOID"; + case "callable": + return "IS_CALLABLE"; + case "iterable": + return "IS_ITERABLE"; + case "mixed": + return "IS_MIXED"; + case "static": + return "IS_STATIC"; + default: + throw new Exception("Not implemented: $this->name"); + } + } + + public function toTypeMask() { + assert($this->isBuiltin); + switch (strtolower($this->name)) { + case "null": + return "MAY_BE_NULL"; + case "false": + return "MAY_BE_FALSE"; + case "bool": + return "MAY_BE_BOOL"; + case "int": + return "MAY_BE_LONG"; + case "float": + return "MAY_BE_DOUBLE"; + case "string": + return "MAY_BE_STRING"; + case "array": + return "MAY_BE_ARRAY"; + case "object": + return "MAY_BE_OBJECT"; + case "callable": + return "MAY_BE_CALLABLE"; + case "mixed": + return "MAY_BE_ANY"; + case "static": + return "MAY_BE_STATIC"; + default: + throw new Exception("Not implemented: $this->name"); + } + } + + public function toEscapedName(): string { + return str_replace('\\', '\\\\', $this->name); + } + + public function equals(SimpleType $other) { + return $this->name === $other->name + && $this->isBuiltin === $other->isBuiltin; + } +} + +class Type { + /** @var SimpleType[] $types */ + public $types; + + public function __construct(array $types) { + $this->types = $types; + } + + public static function fromNode(Node $node): Type { + if ($node instanceof Node\UnionType) { + return new Type(array_map(['SimpleType', 'fromNode'], $node->types)); + } + if ($node instanceof Node\NullableType) { + return new Type([ + SimpleType::fromNode($node->type), + SimpleType::null(), + ]); + } + return new Type([SimpleType::fromNode($node)]); + } + + public static function fromPhpDoc(string $phpDocType) { + $types = explode("|", $phpDocType); + + $simpleTypes = []; + foreach ($types as $type) { + $simpleTypes[] = SimpleType::fromPhpDoc($type); + } + + return new Type($simpleTypes); + } + + public function isNullable(): bool { + foreach ($this->types as $type) { + if ($type->isNull()) { + return true; + } + } + return false; + } + + public function getWithoutNull(): Type { + return new Type(array_filter($this->types, function(SimpleType $type) { + return !$type->isNull(); + })); + } + + public function tryToSimpleType(): ?SimpleType { + $withoutNull = $this->getWithoutNull(); + if (count($withoutNull->types) === 1) { + return $withoutNull->types[0]; + } + return null; + } + + public function toArginfoType(): ?ArginfoType { + $classTypes = []; + $builtinTypes = []; + foreach ($this->types as $type) { + if ($type->isBuiltin) { + $builtinTypes[] = $type; + } else { + $classTypes[] = $type; + } + } + return new ArginfoType($classTypes, $builtinTypes); + } + + public static function equals(?Type $a, ?Type $b): bool { + if ($a === null || $b === null) { + return $a === $b; + } + + if (count($a->types) !== count($b->types)) { + return false; + } + + for ($i = 0; $i < count($a->types); $i++) { + if (!$a->types[$i]->equals($b->types[$i])) { + return false; + } + } + + return true; + } + + public function __toString() { + if ($this->types === null) { + return 'mixed'; + } + + return implode('|', array_map( + function ($type) { return $type->name; }, + $this->types) + ); + } +} + +class ArginfoType { + /** @var ClassType[] $classTypes */ + public $classTypes; + + /** @var SimpleType[] $builtinTypes */ + private $builtinTypes; + + public function __construct(array $classTypes, array $builtinTypes) { + $this->classTypes = $classTypes; + $this->builtinTypes = $builtinTypes; + } + + public function hasClassType(): bool { + return !empty($this->classTypes); + } + + public function toClassTypeString(): string { + return implode('|', array_map(function(SimpleType $type) { + return $type->toEscapedName(); + }, $this->classTypes)); + } + + public function toTypeMask(): string { + if (empty($this->builtinTypes)) { + return '0'; + } + return implode('|', array_map(function(SimpleType $type) { + return $type->toTypeMask(); + }, $this->builtinTypes)); + } +} + +class ArgInfo { + const SEND_BY_VAL = 0; + const SEND_BY_REF = 1; + const SEND_PREFER_REF = 2; + + /** @var string */ + public $name; + /** @var int */ + public $sendBy; + /** @var bool */ + public $isVariadic; + /** @var Type|null */ + public $type; + /** @var Type|null */ + public $phpDocType; + /** @var string|null */ + public $defaultValue; + + public function __construct(string $name, int $sendBy, bool $isVariadic, ?Type $type, ?Type $phpDocType, ?string $defaultValue) { + $this->name = $name; + $this->sendBy = $sendBy; + $this->isVariadic = $isVariadic; + $this->type = $type; + $this->phpDocType = $phpDocType; + $this->defaultValue = $defaultValue; + } + + public function equals(ArgInfo $other): bool { + return $this->name === $other->name + && $this->sendBy === $other->sendBy + && $this->isVariadic === $other->isVariadic + && Type::equals($this->type, $other->type) + && $this->defaultValue === $other->defaultValue; + } + + public function getSendByString(): string { + switch ($this->sendBy) { + case self::SEND_BY_VAL: + return "0"; + case self::SEND_BY_REF: + return "1"; + case self::SEND_PREFER_REF: + return "ZEND_SEND_PREFER_REF"; + } + throw new Exception("Invalid sendBy value"); + } + + public function getMethodSynopsisType(): Type { + if ($this->type) { + return $this->type; + } + + if ($this->phpDocType) { + return $this->phpDocType; + } + + throw new Exception("A parameter must have a type"); + } + + public function hasProperDefaultValue(): bool { + return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN"; + } + + public function getDefaultValueAsArginfoString(): string { + if ($this->hasProperDefaultValue()) { + return '"' . addslashes($this->defaultValue) . '"'; + } + + return "NULL"; + } + + public function getDefaultValueAsMethodSynopsisString(): ?string { + if ($this->defaultValue === null) { + return null; + } + + switch ($this->defaultValue) { + case 'UNKNOWN': + return null; + case 'false': + case 'true': + case 'null': + return "&{$this->defaultValue};"; + } + + return $this->defaultValue; + } +} + +interface FunctionOrMethodName { + public function getDeclaration(): string; + public function getArgInfoName(): string; + public function getMethodSynopsisFilename(): string; + public function __toString(): string; + public function isMethod(): bool; + public function isConstructor(): bool; + public function isDestructor(): bool; +} + +class FunctionName implements FunctionOrMethodName { + /** @var Name */ + private $name; + + public function __construct(Name $name) { + $this->name = $name; + } + + public function getNamespace(): ?string { + if ($this->name->isQualified()) { + return $this->name->slice(0, -1)->toString(); + } + return null; + } + + public function getNonNamespacedName(): string { + if ($this->name->isQualified()) { + throw new Exception("Namespaced name not supported here"); + } + return $this->name->toString(); + } + + public function getDeclarationName(): string { + return $this->name->getLast(); + } + + public function getDeclaration(): string { + return "ZEND_FUNCTION({$this->getDeclarationName()});\n"; + } + + public function getArgInfoName(): string { + $underscoreName = implode('_', $this->name->parts); + return "arginfo_$underscoreName"; + } + + public function getMethodSynopsisFilename(): string { + return implode('_', $this->name->parts); + } + + public function __toString(): string { + return $this->name->toString(); + } + + public function isMethod(): bool { + return false; + } + + public function isConstructor(): bool { + return false; + } + + public function isDestructor(): bool { + return false; + } +} + +class MethodName implements FunctionOrMethodName { + /** @var Name */ + private $className; + /** @var string */ + public $methodName; + + public function __construct(Name $className, string $methodName) { + $this->className = $className; + $this->methodName = $methodName; + } + + public function getDeclarationClassName(): string { + return implode('_', $this->className->parts); + } + + public function getDeclaration(): string { + return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n"; + } + + public function getArgInfoName(): string { + return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}"; + } + + public function getMethodSynopsisFilename(): string { + return $this->getDeclarationClassName() . "_{$this->methodName}"; + } + + public function __toString(): string { + return "$this->className::$this->methodName"; + } + + public function isMethod(): bool { + return true; + } + + public function isConstructor(): bool { + return $this->methodName === "__construct"; + } + + public function isDestructor(): bool { + return $this->methodName === "__destruct"; + } +} + +class ReturnInfo { + /** @var bool */ + public $byRef; + /** @var Type|null */ + public $type; + /** @var Type|null */ + public $phpDocType; + + public function __construct(bool $byRef, ?Type $type, ?Type $phpDocType) { + $this->byRef = $byRef; + $this->type = $type; + $this->phpDocType = $phpDocType; + } + + public function equals(ReturnInfo $other): bool { + return $this->byRef === $other->byRef + && Type::equals($this->type, $other->type); + } + + public function getMethodSynopsisType(): ?Type { + return $this->type ?? $this->phpDocType; + } +} + +class FuncInfo { + /** @var FunctionOrMethodName */ + public $name; + /** @var int */ + public $classFlags; + /** @var int */ + public $flags; + /** @var string|null */ + public $aliasType; + /** @var FunctionName|null */ + public $alias; + /** @var bool */ + public $isDeprecated; + /** @var bool */ + public $verify; + /** @var ArgInfo[] */ + public $args; + /** @var ReturnInfo */ + public $return; + /** @var int */ + public $numRequiredArgs; + /** @var string|null */ + public $cond; + + public function __construct( + FunctionOrMethodName $name, + int $classFlags, + int $flags, + ?string $aliasType, + ?FunctionOrMethodName $alias, + bool $isDeprecated, + bool $verify, + array $args, + ReturnInfo $return, + int $numRequiredArgs, + ?string $cond + ) { + $this->name = $name; + $this->classFlags = $classFlags; + $this->flags = $flags; + $this->aliasType = $aliasType; + $this->alias = $alias; + $this->isDeprecated = $isDeprecated; + $this->verify = $verify; + $this->args = $args; + $this->return = $return; + $this->numRequiredArgs = $numRequiredArgs; + $this->cond = $cond; + } + + public function isMethod(): bool + { + return $this->name->isMethod(); + } + + public function isFinalMethod(): bool + { + return ($this->flags & Class_::MODIFIER_FINAL) || ($this->classFlags & Class_::MODIFIER_FINAL); + } + + public function isInstanceMethod(): bool + { + return !($this->flags & Class_::MODIFIER_STATIC) && $this->isMethod() && !$this->name->isConstructor(); + } + + /** @return string[] */ + public function getModifierNames(): array + { + if (!$this->isMethod()) { + return []; + } + + $result = []; + + if ($this->flags & Class_::MODIFIER_FINAL) { + $result[] = "final"; + } elseif ($this->flags & Class_::MODIFIER_ABSTRACT && $this->classFlags & ~Class_::MODIFIER_ABSTRACT) { + $result[] = "abstract"; + } + + if ($this->flags & Class_::MODIFIER_PROTECTED) { + $result[] = "protected"; + } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { + $result[] = "private"; + } else { + $result[] = "public"; + } + + if ($this->flags & Class_::MODIFIER_STATIC) { + $result[] = "static"; + } + + return $result; + } + + public function hasParamWithUnknownDefaultValue(): bool + { + foreach ($this->args as $arg) { + if ($arg->defaultValue && !$arg->hasProperDefaultValue()) { + return true; + } + } + + return false; + } + + public function equalsApartFromName(FuncInfo $other): bool { + if (count($this->args) !== count($other->args)) { + return false; + } + + for ($i = 0; $i < count($this->args); $i++) { + if (!$this->args[$i]->equals($other->args[$i])) { + return false; + } + } + + return $this->return->equals($other->return) + && $this->numRequiredArgs === $other->numRequiredArgs + && $this->cond === $other->cond; + } + + public function getArgInfoName(): string { + return $this->name->getArgInfoName(); + } + + public function getDeclarationKey(): string + { + $name = $this->alias ?? $this->name; + + return "$name|$this->cond"; + } + + public function getDeclaration(): ?string + { + if ($this->flags & Class_::MODIFIER_ABSTRACT) { + return null; + } + + $name = $this->alias ?? $this->name; + + return $name->getDeclaration(); + } + + public function getFunctionEntry(): string { + if ($this->name instanceof MethodName) { + if ($this->alias) { + if ($this->alias instanceof MethodName) { + return sprintf( + "\tZEND_MALIAS(%s, %s, %s, %s, %s)\n", + $this->alias->getDeclarationClassName(), $this->name->methodName, + $this->alias->methodName, $this->getArgInfoName(), $this->getFlagsAsArginfoString() + ); + } else if ($this->alias instanceof FunctionName) { + return sprintf( + "\tZEND_ME_MAPPING(%s, %s, %s, %s)\n", + $this->name->methodName, $this->alias->getNonNamespacedName(), + $this->getArgInfoName(), $this->getFlagsAsArginfoString() + ); + } else { + throw new Error("Cannot happen"); + } + } else { + $declarationClassName = $this->name->getDeclarationClassName(); + if ($this->flags & Class_::MODIFIER_ABSTRACT) { + return sprintf( + "\tZEND_ABSTRACT_ME_WITH_FLAGS(%s, %s, %s, %s)\n", + $declarationClassName, $this->name->methodName, $this->getArgInfoName(), + $this->getFlagsAsArginfoString() + ); + } + + return sprintf( + "\tZEND_ME(%s, %s, %s, %s)\n", + $declarationClassName, $this->name->methodName, $this->getArgInfoName(), + $this->getFlagsAsArginfoString() + ); + } + } else if ($this->name instanceof FunctionName) { + $namespace = $this->name->getNamespace(); + $declarationName = $this->name->getDeclarationName(); + + if ($this->alias && $this->isDeprecated) { + return sprintf( + "\tZEND_DEP_FALIAS(%s, %s, %s)\n", + $declarationName, $this->alias->getNonNamespacedName(), $this->getArgInfoName() + ); + } + + if ($this->alias) { + return sprintf( + "\tZEND_FALIAS(%s, %s, %s)\n", + $declarationName, $this->alias->getNonNamespacedName(), $this->getArgInfoName() + ); + } + + if ($this->isDeprecated) { + return sprintf( + "\tZEND_DEP_FE(%s, %s)\n", $declarationName, $this->getArgInfoName()); + } + + if ($namespace) { + // Render A\B as "A\\B" in C strings for namespaces + return sprintf( + "\tZEND_NS_FE(\"%s\", %s, %s)\n", + addslashes($namespace), $declarationName, $this->getArgInfoName()); + } else { + return sprintf("\tZEND_FE(%s, %s)\n", $declarationName, $this->getArgInfoName()); + } + } else { + throw new Error("Cannot happen"); + } + } + + private function getFlagsAsArginfoString(): string + { + $flags = "ZEND_ACC_PUBLIC"; + if ($this->flags & Class_::MODIFIER_PROTECTED) { + $flags = "ZEND_ACC_PROTECTED"; + } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { + $flags = "ZEND_ACC_PRIVATE"; + } + + if ($this->flags & Class_::MODIFIER_STATIC) { + $flags .= "|ZEND_ACC_STATIC"; + } + + if ($this->flags & Class_::MODIFIER_FINAL) { + $flags .= "|ZEND_ACC_FINAL"; + } + + if ($this->flags & Class_::MODIFIER_ABSTRACT) { + $flags .= "|ZEND_ACC_ABSTRACT"; + } + + if ($this->isDeprecated) { + $flags .= "|ZEND_ACC_DEPRECATED"; + } + + return $flags; + } + + /** + * @param FuncInfo[] $funcMap + * @param FuncInfo[] $aliasMap + * @throws Exception + */ + public function getMethodSynopsisDocument(array $funcMap, array $aliasMap): ?string { + + $doc = new DOMDocument(); + $doc->formatOutput = true; + $methodSynopsis = $this->getMethodSynopsisElement($funcMap, $aliasMap, $doc); + if (!$methodSynopsis) { + return null; + } + + $doc->appendChild($methodSynopsis); + + return $doc->saveXML(); + } + + /** + * @param FuncInfo[] $funcMap + * @param FuncInfo[] $aliasMap + * @throws Exception + */ + public function getMethodSynopsisElement(array $funcMap, array $aliasMap, DOMDocument $doc): ?DOMElement { + if ($this->hasParamWithUnknownDefaultValue()) { + return null; + } + + if ($this->name->isConstructor()) { + $synopsisType = "constructorsynopsis"; + } elseif ($this->name->isDestructor()) { + $synopsisType = "destructorsynopsis"; + } else { + $synopsisType = "methodsynopsis"; + } + + $methodSynopsis = $doc->createElement($synopsisType); + + $aliasedFunc = $this->aliasType === "alias" && isset($funcMap[$this->alias->__toString()]) ? $funcMap[$this->alias->__toString()] : null; + $aliasFunc = $aliasMap[$this->name->__toString()] ?? null; + + if (($this->aliasType === "alias" && $aliasedFunc !== null && $aliasedFunc->isMethod() !== $this->isMethod()) || + ($aliasFunc !== null && $aliasFunc->isMethod() !== $this->isMethod()) + ) { + $role = $doc->createAttribute("role"); + $role->value = $this->isMethod() ? "oop" : "procedural"; + $methodSynopsis->appendChild($role); + } + + $methodSynopsis->appendChild(new DOMText("\n ")); + + foreach ($this->getModifierNames() as $modifierString) { + $modifierElement = $doc->createElement('modifier', $modifierString); + $methodSynopsis->appendChild($modifierElement); + $methodSynopsis->appendChild(new DOMText(" ")); + } + + $returnType = $this->return->getMethodSynopsisType(); + if ($returnType) { + $this->appendMethodSynopsisTypeToElement($doc, $methodSynopsis, $returnType); + } + + $methodname = $doc->createElement('methodname', $this->name->__toString()); + $methodSynopsis->appendChild($methodname); + + if (empty($this->args)) { + $methodSynopsis->appendChild(new DOMText("\n ")); + $void = $doc->createElement('void'); + $methodSynopsis->appendChild($void); + } else { + foreach ($this->args as $arg) { + $methodSynopsis->appendChild(new DOMText("\n ")); + $methodparam = $doc->createElement('methodparam'); + if ($arg->defaultValue !== null) { + $methodparam->setAttribute("choice", "opt"); + } + if ($arg->isVariadic) { + $methodparam->setAttribute("rep", "repeat"); + } + + $methodSynopsis->appendChild($methodparam); + $this->appendMethodSynopsisTypeToElement($doc, $methodparam, $arg->getMethodSynopsisType()); + + $parameter = $doc->createElement('parameter', $arg->name); + if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) { + $parameter->setAttribute("role", "reference"); + } + + $methodparam->appendChild($parameter); + $defaultValue = $arg->getDefaultValueAsMethodSynopsisString(); + if ($defaultValue !== null) { + $initializer = $doc->createElement('initializer'); + if (preg_match('/^[a-zA-Z_][a-zA-Z_0-9]*$/', $defaultValue)) { + $constant = $doc->createElement('constant', $defaultValue); + $initializer->appendChild($constant); + } else { + $initializer->nodeValue = $defaultValue; + } + $methodparam->appendChild($initializer); + } + } + } + $methodSynopsis->appendChild(new DOMText("\n ")); + + return $methodSynopsis; + } + + public function discardInfoForOldPhpVersions(): void { + $this->return->type = null; + foreach ($this->args as $arg) { + $arg->type = null; + $arg->defaultValue = null; + } + } + + private function appendMethodSynopsisTypeToElement(DOMDocument $doc, DOMElement $elementToAppend, Type $type) { + if (count($type->types) > 1) { + $typeElement = $doc->createElement('type'); + $typeElement->setAttribute("class", "union"); + + foreach ($type->types as $type) { + $unionTypeElement = $doc->createElement('type', $type->name); + $typeElement->appendChild($unionTypeElement); + } + } else { + $typeElement = $doc->createElement('type', $type->types[0]->name); + } + + $elementToAppend->appendChild($typeElement); + } +} + +class ClassInfo { + /** @var Name */ + public $name; + /** @var FuncInfo[] */ + public $funcInfos; + + public function __construct(Name $name, array $funcInfos) { + $this->name = $name; + $this->funcInfos = $funcInfos; + } +} + +class FileInfo { + /** @var FuncInfo[] */ + public $funcInfos = []; + /** @var ClassInfo[] */ + public $classInfos = []; + /** @var bool */ + public $generateFunctionEntries = false; + /** @var string */ + public $declarationPrefix = ""; + /** @var bool */ + public $generateLegacyArginfo = false; + + /** + * @return iterable<FuncInfo> + */ + public function getAllFuncInfos(): iterable { + yield from $this->funcInfos; + foreach ($this->classInfos as $classInfo) { + yield from $classInfo->funcInfos; + } + } +} + +class DocCommentTag { + /** @var string */ + public $name; + /** @var string|null */ + public $value; + + public function __construct(string $name, ?string $value) { + $this->name = $name; + $this->value = $value; + } + + public function getValue(): string { + if ($this->value === null) { + throw new Exception("@$this->name does not have a value"); + } + + return $this->value; + } + + public function getType(): string { + $value = $this->getValue(); + + $matches = []; + + if ($this->name === "param") { + preg_match('/^\s*([\w\|\\\\\[\]]+)\s*\$\w+.*$/', $value, $matches); + } elseif ($this->name === "return") { + preg_match('/^\s*([\w\|\\\\\[\]]+)\s*$/', $value, $matches); + } + + if (isset($matches[1]) === false) { + throw new Exception("@$this->name doesn't contain a type or has an invalid format \"$value\""); + } + + return $matches[1]; + } + + public function getVariableName(): string { + $value = $this->value; + if ($value === null || strlen($value) === 0) { + throw new Exception("@$this->name doesn't have any value"); + } + + $matches = []; + + if ($this->name === "param") { + preg_match('/^\s*[\w\|\\\\\[\]]+\s*\$(\w+).*$/', $value, $matches); + } elseif ($this->name === "prefer-ref") { + preg_match('/^\s*\$(\w+).*$/', $value, $matches); + } + + if (isset($matches[1]) === false) { + throw new Exception("@$this->name doesn't contain a variable name or has an invalid format \"$value\""); + } + + return $matches[1]; + } +} + +/** @return DocCommentTag[] */ +function parseDocComment(DocComment $comment): array { + $commentText = substr($comment->getText(), 2, -2); + $tags = []; + foreach (explode("\n", $commentText) as $commentLine) { + $regex = '/^\*\s*@([a-z-]+)(?:\s+(.+))?$/'; + if (preg_match($regex, trim($commentLine), $matches)) { + $tags[] = new DocCommentTag($matches[1], $matches[2] ?? null); + } + } + + return $tags; +} + +function parseFunctionLike( + PrettyPrinterAbstract $prettyPrinter, + FunctionOrMethodName $name, + int $classFlags, + int $flags, + Node\FunctionLike $func, + ?string $cond +): FuncInfo { + $comment = $func->getDocComment(); + $paramMeta = []; + $aliasType = null; + $alias = null; + $isDeprecated = false; + $verify = true; + $docReturnType = null; + $docParamTypes = []; + + if ($comment) { + $tags = parseDocComment($comment); + foreach ($tags as $tag) { + if ($tag->name === 'prefer-ref') { + $varName = $tag->getVariableName(); + if (!isset($paramMeta[$varName])) { + $paramMeta[$varName] = []; + } + $paramMeta[$varName]['preferRef'] = true; + } else if ($tag->name === 'alias' || $tag->name === 'implementation-alias') { + $aliasType = $tag->name; + $aliasParts = explode("::", $tag->getValue()); + if (count($aliasParts) === 1) { + $alias = new FunctionName(new Name($aliasParts[0])); + } else { + $alias = new MethodName(new Name($aliasParts[0]), $aliasParts[1]); + } + } else if ($tag->name === 'deprecated') { + $isDeprecated = true; + } else if ($tag->name === 'no-verify') { + $verify = false; + } else if ($tag->name === 'return') { + $docReturnType = $tag->getType(); + } else if ($tag->name === 'param') { + $docParamTypes[$tag->getVariableName()] = $tag->getType(); + } + } + } + + $varNameSet = []; + $args = []; + $numRequiredArgs = 0; + $foundVariadic = false; + foreach ($func->getParams() as $i => $param) { + $varName = $param->var->name; + $preferRef = !empty($paramMeta[$varName]['preferRef']); + unset($paramMeta[$varName]); + + if (isset($varNameSet[$varName])) { + throw new Exception("Duplicate parameter name $varName for function $name"); + } + $varNameSet[$varName] = true; + + if ($preferRef) { + $sendBy = ArgInfo::SEND_PREFER_REF; + } else if ($param->byRef) { + $sendBy = ArgInfo::SEND_BY_REF; + } else { + $sendBy = ArgInfo::SEND_BY_VAL; + } + + if ($foundVariadic) { + throw new Exception("Error in function $name: only the last parameter can be variadic"); + } + + $type = $param->type ? Type::fromNode($param->type) : null; + if ($type === null && !isset($docParamTypes[$varName])) { + throw new Exception("Missing parameter type for function $name()"); + } + + if ($param->default instanceof Expr\ConstFetch && + $param->default->name->toLowerString() === "null" && + $type && !$type->isNullable() + ) { + $simpleType = $type->tryToSimpleType(); + if ($simpleType === null) { + throw new Exception( + "Parameter $varName of function $name has null default, but is not nullable"); + } + } + + $foundVariadic = $param->variadic; + + $args[] = new ArgInfo( + $varName, + $sendBy, + $param->variadic, + $type, + isset($docParamTypes[$varName]) ? Type::fromPhpDoc($docParamTypes[$varName]) : null, + $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null + ); + if (!$param->default && !$param->variadic) { + $numRequiredArgs = $i + 1; + } + } + + foreach (array_keys($paramMeta) as $var) { + throw new Exception("Found metadata for invalid param $var of function $name"); + } + + $returnType = $func->getReturnType(); + if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { + throw new Exception("Missing return type for function $name()"); + } + + $return = new ReturnInfo( + $func->returnsByRef(), + $returnType ? Type::fromNode($returnType) : null, + $docReturnType ? Type::fromPhpDoc($docReturnType) : null + ); + + return new FuncInfo( + $name, + $classFlags, + $flags, + $aliasType, + $alias, + $isDeprecated, + $verify, + $args, + $return, + $numRequiredArgs, + $cond + ); +} + +function handlePreprocessorConditions(array &$conds, Stmt $stmt): ?string { + foreach ($stmt->getComments() as $comment) { + $text = trim($comment->getText()); + if (preg_match('/^#\s*if\s+(.+)$/', $text, $matches)) { + $conds[] = $matches[1]; + } else if (preg_match('/^#\s*ifdef\s+(.+)$/', $text, $matches)) { + $conds[] = "defined($matches[1])"; + } else if (preg_match('/^#\s*ifndef\s+(.+)$/', $text, $matches)) { + $conds[] = "!defined($matches[1])"; + } else if (preg_match('/^#\s*else$/', $text)) { + if (empty($conds)) { + throw new Exception("Encountered else without corresponding #if"); + } + $cond = array_pop($conds); + $conds[] = "!($cond)"; + } else if (preg_match('/^#\s*endif$/', $text)) { + if (empty($conds)) { + throw new Exception("Encountered #endif without corresponding #if"); + } + array_pop($conds); + } else if ($text[0] === '#') { + throw new Exception("Unrecognized preprocessor directive \"$text\""); + } + } + + return empty($conds) ? null : implode(' && ', $conds); +} + +function getFileDocComment(array $stmts): ?DocComment { + if (empty($stmts)) { + return null; + } + + $comments = $stmts[0]->getComments(); + if (empty($comments)) { + return null; + } + + if ($comments[0] instanceof DocComment) { + return $comments[0]; + } + + return null; +} + +function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstract $prettyPrinter) { + $conds = []; + foreach ($stmts as $stmt) { + if ($stmt instanceof Stmt\Nop) { + continue; + } + + if ($stmt instanceof Stmt\Namespace_) { + handleStatements($fileInfo, $stmt->stmts, $prettyPrinter); + continue; + } + + $cond = handlePreprocessorConditions($conds, $stmt); + if ($stmt instanceof Stmt\Function_) { + $fileInfo->funcInfos[] = parseFunctionLike( + $prettyPrinter, + new FunctionName($stmt->namespacedName), + 0, + 0, + $stmt, + $cond + ); + continue; + } + + if ($stmt instanceof Stmt\ClassLike) { + $className = $stmt->namespacedName; + $methodInfos = []; + foreach ($stmt->stmts as $classStmt) { + $cond = handlePreprocessorConditions($conds, $classStmt); + if ($classStmt instanceof Stmt\Nop) { + continue; + } + + if (!$classStmt instanceof Stmt\ClassMethod) { + throw new Exception("Not implemented {$classStmt->getType()}"); + } + + $classFlags = 0; + if ($stmt instanceof Class_) { + $classFlags = $stmt->flags; + } + + $flags = $classStmt->flags; + if ($stmt instanceof Stmt\Interface_) { + $flags |= Class_::MODIFIER_ABSTRACT; + } + + if (!($flags & Class_::VISIBILITY_MODIFIER_MASK)) { + throw new Exception("Method visibility modifier is required"); + } + + $methodInfos[] = parseFunctionLike( + $prettyPrinter, + new MethodName($className, $classStmt->name->toString()), + $classFlags, + $flags, + $classStmt, + $cond + ); + } + + $fileInfo->classInfos[] = new ClassInfo($className, $methodInfos); + continue; + } + + throw new Exception("Unexpected node {$stmt->getType()}"); + } +} + +function parseStubFile(string $code): FileInfo { + $lexer = new PhpParser\Lexer(); + $parser = new PhpParser\Parser\Php7($lexer); + $nodeTraverser = new PhpParser\NodeTraverser; + $nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); + $prettyPrinter = new class extends Standard { + protected function pName_FullyQualified(Name\FullyQualified $node) { + return implode('\\', $node->parts); + } + }; + + $stmts = $parser->parse($code); + $nodeTraverser->traverse($stmts); + + $fileInfo = new FileInfo; + $fileDocComment = getFileDocComment($stmts); + if ($fileDocComment) { + $fileTags = parseDocComment($fileDocComment); + foreach ($fileTags as $tag) { + if ($tag->name === 'generate-function-entries') { + $fileInfo->generateFunctionEntries = true; + $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; + } else if ($tag->name === 'generate-legacy-arginfo') { + $fileInfo->generateLegacyArginfo = true; + } + } + } + + handleStatements($fileInfo, $stmts, $prettyPrinter); + return $fileInfo; +} + +function funcInfoToCode(FuncInfo $funcInfo, bool $minimal): string { + $code = ''; + + // Generate the minimal, most compatible arginfo across PHP versions + if ($minimal) { + $code .= sprintf("ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n", + $funcInfo->getArgInfoName(), + $funcInfo->return->byRef, + $funcInfo->numRequiredArgs); + foreach ($funcInfo->args as $argInfo) { + $code .= sprintf("\tZEND_ARG_INFO(0, %s)\n", $argInfo->name); + } + } else { + $returnType = $funcInfo->return->type; + if ($returnType !== null) { + if (null !== $simpleReturnType = $returnType->tryToSimpleType()) { + if ($simpleReturnType->isBuiltin) { + $code .= sprintf( + "AWS_PHP_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(%s, %d, %d, %s, %d)\n", + $funcInfo->getArgInfoName(), $funcInfo->return->byRef, + $funcInfo->numRequiredArgs, + $simpleReturnType->toTypeCode(), $returnType->isNullable() + ); + } else { + $code .= sprintf( + "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(%s, %d, %d, %s, %d)\n", + $funcInfo->getArgInfoName(), $funcInfo->return->byRef, + $funcInfo->numRequiredArgs, + $simpleReturnType->toEscapedName(), $returnType->isNullable() + ); + } + } else { + $arginfoType = $returnType->toArginfoType(); + if ($arginfoType->hasClassType()) { + $code .= sprintf( + "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(%s, %d, %d, %s, %s)\n", + $funcInfo->getArgInfoName(), $funcInfo->return->byRef, + $funcInfo->numRequiredArgs, + $arginfoType->toClassTypeString(), $arginfoType->toTypeMask() + ); + } else { + $code .= sprintf( + "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(%s, %d, %d, %s)\n", + $funcInfo->getArgInfoName(), $funcInfo->return->byRef, + $funcInfo->numRequiredArgs, + $arginfoType->toTypeMask() + ); + } + } + } else { + $code .= sprintf( + "ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n", + $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs + ); + } + + foreach ($funcInfo->args as $argInfo) { + $argKind = $argInfo->isVariadic ? "ARG_VARIADIC" : "ARG"; + $argDefaultKind = $argInfo->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : ""; + $argType = $argInfo->type; + if ($argType !== null) { + if (null !== $simpleArgType = $argType->tryToSimpleType()) { + if ($simpleArgType->isBuiltin) { + $code .= sprintf( + "\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n", + $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, + $simpleArgType->toTypeCode(), $argType->isNullable(), + $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" + ); + } else { + $code .= sprintf( + "\tZEND_%s_OBJ_INFO%s(%s, %s, %s, %d%s)\n", + $argKind,$argDefaultKind, $argInfo->getSendByString(), $argInfo->name, + $simpleArgType->toEscapedName(), $argType->isNullable(), + $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" + ); + } + } else { + $arginfoType = $argType->toArginfoType(); + if ($arginfoType->hasClassType()) { + $code .= sprintf( + "\tZEND_%s_OBJ_TYPE_MASK(%s, %s, %s, %s, %s)\n", + $argKind, $argInfo->getSendByString(), $argInfo->name, + $arginfoType->toClassTypeString(), $arginfoType->toTypeMask(), + $argInfo->getDefaultValueAsArginfoString() + ); + } else { + $code .= sprintf( + "\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n", + $argKind, $argInfo->getSendByString(), $argInfo->name, + $arginfoType->toTypeMask(), + $argInfo->getDefaultValueAsArginfoString() + ); + } + } + } else { + $code .= sprintf( + "\tZEND_%s_INFO%s(%s, %s%s)\n", + $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, + $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" + ); + } + } + } + + $code .= "ZEND_END_ARG_INFO()"; + return $code . "\n"; +} + +/** @param FuncInfo[] $generatedFuncInfos */ +function findEquivalentFuncInfo(array $generatedFuncInfos, FuncInfo $funcInfo): ?FuncInfo { + foreach ($generatedFuncInfos as $generatedFuncInfo) { + if ($generatedFuncInfo->equalsApartFromName($funcInfo)) { + return $generatedFuncInfo; + } + } + return null; +} + +/** @param iterable<FuncInfo> $funcInfos */ +function generateCodeWithConditions( + iterable $funcInfos, string $separator, Closure $codeGenerator): string { + $code = ""; + foreach ($funcInfos as $funcInfo) { + $funcCode = $codeGenerator($funcInfo); + if ($funcCode === null) { + continue; + } + + $code .= $separator; + if ($funcInfo->cond) { + $code .= "#if {$funcInfo->cond}\n"; + $code .= $funcCode; + $code .= "#endif\n"; + } else { + $code .= $funcCode; + } + } + return $code; +} + +function generateArgInfoCode(FileInfo $fileInfo, string $stubHash, bool $minimal): string { + $code = "/* This is a generated file, edit the .stub.php file instead.\n" + . " * Stub hash: $stubHash */\n"; + $generatedFuncInfos = []; + $code .= generateCodeWithConditions( + $fileInfo->getAllFuncInfos(), "\n", + function (FuncInfo $funcInfo) use(&$generatedFuncInfos, $minimal) { + /* If there already is an equivalent arginfo structure, only emit a #define */ + if ($generatedFuncInfo = findEquivalentFuncInfo($generatedFuncInfos, $funcInfo)) { + $code = sprintf( + "#define %s %s\n", + $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() + ); + } else { + $code = funcInfoToCode($funcInfo, $minimal); + } + + $generatedFuncInfos[] = $funcInfo; + return $code; + } + ); + + if ($fileInfo->generateFunctionEntries) { + $code .= "\n\n"; + + $generatedFunctionDeclarations = []; + $code .= generateCodeWithConditions( + $fileInfo->getAllFuncInfos(), "", + function (FuncInfo $funcInfo) use($fileInfo, &$generatedFunctionDeclarations) { + $key = $funcInfo->getDeclarationKey(); + if (isset($generatedFunctionDeclarations[$key])) { + return null; + } + + $generatedFunctionDeclarations[$key] = true; + return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); + } + ); + + if (!empty($fileInfo->funcInfos)) { + $code .= generateFunctionEntries(null, $fileInfo->funcInfos); + } + + foreach ($fileInfo->classInfos as $classInfo) { + $code .= generateFunctionEntries($classInfo->name, $classInfo->funcInfos); + } + } + + return $code; +} + +/** @param FuncInfo[] $funcInfos */ +function generateFunctionEntries(?Name $className, array $funcInfos): string { + $code = ""; + + $functionEntryName = "ext_functions"; + if ($className) { + $underscoreName = implode("_", $className->parts); + $functionEntryName = "class_{$underscoreName}_methods"; + } + + $code .= "\n\nstatic const zend_function_entry {$functionEntryName}[] = {\n"; + $code .= generateCodeWithConditions($funcInfos, "", function (FuncInfo $funcInfo) { + return $funcInfo->getFunctionEntry(); + }); + $code .= "\tZEND_FE_END\n"; + $code .= "};\n"; + + return $code; +} + +/** + * @param FuncInfo[] $funcMap + * @param FuncInfo[] $aliasMap + * @return array<string, string> + */ +function generateMethodSynopses(array $funcMap, array $aliasMap): array { + $result = []; + + foreach ($funcMap as $funcInfo) { + $methodSynopsis = $funcInfo->getMethodSynopsisDocument($funcMap, $aliasMap); + if ($methodSynopsis !== null) { + $result[$funcInfo->name->getMethodSynopsisFilename() . ".xml"] = $methodSynopsis; + } + } + + return $result; +} + +/** + * @param FuncInfo[] $funcMap + * @param FuncInfo[] $aliasMap + * @return array<string, string> + */ +function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $aliasMap): array { + $methodSynopses = []; + + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($targetDirectory), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($it as $file) { + $pathName = $file->getPathName(); + if (!preg_match('/\.xml$/i', $pathName)) { + continue; + } + + $xml = file_get_contents($pathName); + if ($xml === false) { + continue; + } + + if (stripos($xml, "<methodsynopsis") === false && stripos($xml, "<constructorsynopsis") === false && stripos($xml, "<destructorsynopsis") === false) { + continue; + } + + $replacedXml = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml); + + $doc = new DOMDocument(); + $doc->formatOutput = false; + $doc->preserveWhiteSpace = true; + $doc->validateOnParse = true; + $success = $doc->loadXML($replacedXml); + if (!$success) { + echo "Failed opening $pathName\n"; + continue; + } + + $docComparator = new DOMDocument(); + $docComparator->preserveWhiteSpace = false; + $docComparator->formatOutput = true; + + $methodSynopsisElements = []; + foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) { + $methodSynopsisElements[] = $element; + } + foreach ($doc->getElementsByTagName("destructorsynopsis") as $element) { + $methodSynopsisElements[] = $element; + } + foreach ($doc->getElementsByTagName("methodsynopsis") as $element) { + $methodSynopsisElements[] = $element; + } + + foreach ($methodSynopsisElements as $methodSynopsis) { + if (!$methodSynopsis instanceof DOMElement) { + continue; + } + + $list = $methodSynopsis->getElementsByTagName("methodname"); + $item = $list->item(0); + if (!$item instanceof DOMElement) { + continue; + } + $funcName = $item->textContent; + if (!isset($funcMap[$funcName])) { + continue; + } + $funcInfo = $funcMap[$funcName]; + + $newMethodSynopsis = $funcInfo->getMethodSynopsisElement($funcMap, $aliasMap, $doc); + if ($newMethodSynopsis === null) { + continue; + } + + // Retrieve current signature + + $params = []; + $list = $methodSynopsis->getElementsByTagName("methodparam"); + foreach ($list as $i => $item) { + if (!$item instanceof DOMElement) { + continue; + } + + $paramList = $item->getElementsByTagName("parameter"); + if ($paramList->count() !== 1) { + continue; + } + + $paramName = $paramList->item(0)->textContent; + $paramTypes = []; + + $paramList = $item->getElementsByTagName("type"); + foreach ($paramList as $type) { + if (!$type instanceof DOMElement) { + continue; + } + + $paramTypes[] = $type->textContent; + } + + $params[$paramName] = ["index" => $i, "type" => $paramTypes]; + } + + // Check if there is any change - short circuit if there is not any. + + $xml1 = $doc->saveXML($methodSynopsis); + $xml1 = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml1); + $docComparator->loadXML($xml1); + $xml1 = $docComparator->saveXML(); + + $methodSynopsis->parentNode->replaceChild($newMethodSynopsis, $methodSynopsis); + + $xml2 = $doc->saveXML($newMethodSynopsis); + $xml2 = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml2); + $docComparator->loadXML($xml2); + $xml2 = $docComparator->saveXML(); + + if ($xml1 === $xml2) { + continue; + } + + // Update parameter references + + $paramList = $doc->getElementsByTagName("parameter"); + /** @var DOMElement $paramElement */ + foreach ($paramList as $paramElement) { + if ($paramElement->parentNode && $paramElement->parentNode->nodeName === "methodparam") { + continue; + } + + $name = $paramElement->textContent; + if (!isset($params[$name])) { + continue; + } + + $index = $params[$name]["index"]; + if (!isset($funcInfo->args[$index])) { + continue; + } + + $paramElement->textContent = $funcInfo->args[$index]->name; + } + + // Return the updated XML + + $replacedXml = $doc->saveXML(); + + $replacedXml = preg_replace( + [ + "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", + "/<refentry\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", + "/<refentry\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xlink=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", + ], + [ + "&$1", + "<refentry xml:id=\"$2\" xmlns=\"$1\">", + "<refentry xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", + ], + $replacedXml + ); + + $methodSynopses[$pathName] = $replacedXml; + } + } + + return $methodSynopses; +} + +function installPhpParser(string $version, string $phpParserDir) { + $lockFile = __DIR__ . "/PHP-Parser-install-lock"; + $lockFd = fopen($lockFile, 'w+'); + if (!flock($lockFd, LOCK_EX)) { + throw new Exception("Failed to acquire installation lock"); + } + + try { + // Check whether a parallel process has already installed PHP-Parser. + if (is_dir($phpParserDir)) { + return; + } + + $cwd = getcwd(); + chdir(__DIR__); + + $tarName = "v$version.tar.gz"; + passthru("wget https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); + if ($exit !== 0) { + passthru("curl -LO https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); + } + if ($exit !== 0) { + throw new Exception("Failed to download PHP-Parser tarball"); + } + if (!mkdir($phpParserDir)) { + throw new Exception("Failed to create directory $phpParserDir"); + } + passthru("tar xvzf $tarName -C PHP-Parser-$version --strip-components 1", $exit); + if ($exit !== 0) { + throw new Exception("Failed to extract PHP-Parser tarball"); + } + unlink(__DIR__ . "/$tarName"); + chdir($cwd); + } finally { + flock($lockFd, LOCK_UN); + @unlink($lockFile); + } +} + +function initPhpParser() { + static $isInitialized = false; + if ($isInitialized) { + return; + } + + if (!extension_loaded("tokenizer")) { + throw new Exception("The \"tokenizer\" extension is not available"); + } + + $isInitialized = true; + $version = "4.9.0"; + $phpParserDir = __DIR__ . "/PHP-Parser-$version"; + if (!is_dir($phpParserDir)) { + installPhpParser($version, $phpParserDir); + } + + spl_autoload_register(function(string $class) use($phpParserDir) { + if (strpos($class, "PhpParser\\") === 0) { + $fileName = $phpParserDir . "/lib/" . str_replace("\\", "/", $class) . ".php"; + require $fileName; + } + }); +} + +$optind = null; +$options = getopt("fh", [ + "force-regeneration", + "parameter-stats", + "help", + "verify", + "generate-methodsynopses", + "replace-methodsynopses", + "minimal-arginfo"], $optind); + +$context = new Context; +$printParameterStats = isset($options["parameter-stats"]); +$verify = isset($options["verify"]); +$generateMethodSynopses = isset($options["generate-methodsynopses"]); +$replaceMethodSynopses = isset($options["replace-methodsynopses"]); +$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]); +$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateMethodSynopses || $replaceMethodSynopses; +$context->minimalArgInfo = isset($options["minimal-arginfo"]); +$targetMethodSynopses = $argv[$optind + 1] ?? null; +if ($replaceMethodSynopses && $targetMethodSynopses === null) { + die("A target directory must be provided.\n"); +} + +if (isset($options["h"]) || isset($options["help"])) { + die("\nusage: gen-stub.php [ -f | --force-regeneration ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]\n\n"); +} + +$fileInfos = []; +$location = $argv[$optind] ?? "."; +if (is_file($location)) { + // Generate single file. + $fileInfo = processStubFile($location, $context); + if ($fileInfo) { + $fileInfos[] = $fileInfo; + } +} else if (is_dir($location)) { + $fileInfos = processDirectory($location, $context); +} else { + echo "$location is neither a file nor a directory.\n"; + exit(1); +} + +if ($printParameterStats) { + $parameterStats = []; + + foreach ($fileInfos as $fileInfo) { + foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { + foreach ($funcInfo->args as $argInfo) { + if (!isset($parameterStats[$argInfo->name])) { + $parameterStats[$argInfo->name] = 0; + } + $parameterStats[$argInfo->name]++; + } + } + } + + arsort($parameterStats); + echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n"; +} + +/** @var FuncInfo[] $funcMap */ +$funcMap = []; +/** @var FuncInfo[] $aliasMap */ +$aliasMap = []; + +foreach ($fileInfos as $fileInfo) { + foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { + /** @var FuncInfo $funcInfo */ + $funcMap[$funcInfo->name->__toString()] = $funcInfo; + + if ($funcInfo->aliasType === "alias") { + $aliasMap[$funcInfo->alias->__toString()] = $funcInfo; + } + } +} + +if ($verify) { + $errors = []; + + foreach ($aliasMap as $aliasFunc) { + if (!isset($funcMap[$aliasFunc->alias->__toString()])) { + $errors[] = "Aliased function {$aliasFunc->alias}() cannot be found"; + continue; + } + + if (!$aliasFunc->verify) { + continue; + } + + $aliasedFunc = $funcMap[$aliasFunc->alias->__toString()]; + $aliasedArgs = $aliasedFunc->args; + $aliasArgs = $aliasFunc->args; + + if ($aliasFunc->isInstanceMethod() !== $aliasedFunc->isInstanceMethod()) { + if ($aliasFunc->isInstanceMethod()) { + $aliasedArgs = array_slice($aliasedArgs, 1); + } + + if ($aliasedFunc->isInstanceMethod()) { + $aliasArgs = array_slice($aliasArgs, 1); + } + } + + array_map( + function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { + if ($aliasArg === null) { + assert($aliasedArg !== null); + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; + return null; + } + + if ($aliasedArg === null) { + $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; + return null; + } + + if ($aliasArg->name !== $aliasedArg->name) { + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; + return null; + } + + if ($aliasArg->type != $aliasedArg->type) { + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; + } + + if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; + } + }, + $aliasArgs, $aliasedArgs + ); + + if ((!$aliasedFunc->isMethod() || $aliasedFunc->isFinalMethod()) && + (!$aliasFunc->isMethod() || $aliasFunc->isFinalMethod()) && + $aliasFunc->return != $aliasedFunc->return + ) { + $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same return type"; + } + } + + echo implode("\n", $errors); + if (!empty($errors)) { + echo "\n"; + exit(1); + } +} + +if ($generateMethodSynopses) { + $methodSynopsesDirectory = getcwd() . "/methodsynopses"; + + $methodSynopses = generateMethodSynopses($funcMap, $aliasMap); + if (!empty($methodSynopses)) { + if (!file_exists($methodSynopsesDirectory)) { + mkdir($methodSynopsesDirectory); + } + + foreach ($methodSynopses as $filename => $content) { + if (file_put_contents("$methodSynopsesDirectory/$filename", $content)) { + echo "Saved $filename\n"; + } + } + } +} + +if ($replaceMethodSynopses) { + $methodSynopses = replaceMethodSynopses($targetMethodSynopses, $funcMap, $aliasMap); + + foreach ($methodSynopses as $filename => $content) { + if (file_put_contents($filename, $content)) { + echo "Saved $filename\n"; + } + } +} |