Skip to content

Commit

Permalink
Add support for generating methodsynopses from stubs
Browse files Browse the repository at this point in the history
  • Loading branch information
kocsismate committed Oct 22, 2020
1 parent ac87880 commit fca45e9
Show file tree
Hide file tree
Showing 33 changed files with 495 additions and 19 deletions.
210 changes: 197 additions & 13 deletions build/gen_stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ function processStubFile(string $stubFile, Context $context): ?FileInfo {
echo "Saved $arginfoFile\n";
}

if ($context->generateMethodsynopses && $fileInfo->generateMethodsynopses) {
$methodsynopsesDirectory = dirname($stubFile) . "/methodsynopses";

$methodsynopses = generateMethodsynopses($fileInfo);
if (!empty($methodsynopses) && ($context->forceRegeneration || $stubHash !== $oldStubHash)) {
if (!file_exists($methodsynopsesDirectory)) {
mkdir($methodsynopsesDirectory);
}

foreach ($methodsynopses as $filename => $content) {
if (file_put_contents("$methodsynopsesDirectory/$filename", $content)) {
echo "Saved $filename\n";
}
}
}
}

if ($fileInfo->generateLegacyArginfo) {
foreach ($fileInfo->getAllFuncInfos() as $funcInfo) {
$funcInfo->discardInfoForOldPhpVersions();
Expand Down Expand Up @@ -98,6 +115,8 @@ class Context {
public $forceParse = false;
/** @var bool */
public $forceRegeneration = false;
/** @var bool */
public $generateMethodsynopses = false;
}

class SimpleType {
Expand Down Expand Up @@ -278,6 +297,17 @@ public static function equals(?Type $a, ?Type $b): bool {

return true;
}

public function __toString() {
if ($this->types === null) {
return 'mixed';
}

return implode('|', array_map(
function ($type) {return $type->name;},
$this->types)
);
}
}

class RepresentableType {
Expand Down Expand Up @@ -342,22 +372,48 @@ public function getSendByString(): string {
throw new Exception("Invalid sendBy value");
}

public function getTypeAsMethodsynopsisString(): string {
if ($this->type === null) {
return 'mixed';
}

return $this->type->__toString();
}

public function hasDefaultValue(): bool {
return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN";
}

public function getDefaultValueString(): string {
public function getDefaultValueAsArginfoString(): string {
if ($this->hasDefaultValue()) {
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 isMagicMethod(): bool;
public function isMethod(): bool;
Expand Down Expand Up @@ -399,6 +455,10 @@ public function getArgInfoName(): string {
return "arginfo_$underscoreName";
}

public function getMethodsynopsisFilename(): string {
return implode('_', $this->name->parts);
}

public function __toString(): string {
return $this->name->toString();
}
Expand Down Expand Up @@ -439,6 +499,10 @@ 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";
}
Expand Down Expand Up @@ -471,6 +535,14 @@ public function equals(ReturnInfo $other): bool {
return $this->byRef === $other->byRef
&& Type::equals($this->type, $other->type);
}

public function getTypeAsMethodsynopsisString(): string {
if ($this->type === null) {
return 'mixed';
}

return $this->type->__toString();
}
}

class FuncInfo {
Expand Down Expand Up @@ -538,6 +610,32 @@ public function isInstanceMethod(): bool
return !($this->flags & Class_::MODIFIER_STATIC) && $this->isMethod() && !$this->name->isConstructor();
}

/** @return string[] */
public function getModifierNames(): array
{
$result = [];

if ($this->flags & Class_::MODIFIER_FINAL) {
$result[] = "final";
} elseif ($this->flags & 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 equalsApartFromName(FuncInfo $other): bool {
if (count($this->args) !== count($other->args)) {
return false;
Expand Down Expand Up @@ -583,13 +681,13 @@ public function getFunctionEntry(): string {
return sprintf(
"\tZEND_MALIAS(%s, %s, %s, %s, %s)\n",
$this->alias->getDeclarationClassName(), $this->name->methodName,
$this->alias->methodName, $this->getArgInfoName(), $this->getFlagsAsString()
$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->getFlagsAsString()
$this->getArgInfoName(), $this->getFlagsAsArginfoString()
);
} else {
throw new Error("Cannot happen");
Expand All @@ -600,14 +698,14 @@ public function getFunctionEntry(): string {
return sprintf(
"\tZEND_ABSTRACT_ME_WITH_FLAGS(%s, %s, %s, %s)\n",
$declarationClassName, $this->name->methodName, $this->getArgInfoName(),
$this->getFlagsAsString()
$this->getFlagsAsArginfoString()
);
}

return sprintf(
"\tZEND_ME(%s, %s, %s, %s)\n",
$declarationClassName, $this->name->methodName, $this->getArgInfoName(),
$this->getFlagsAsString()
$this->getFlagsAsArginfoString()
);
}
} else if ($this->name instanceof FunctionName) {
Expand Down Expand Up @@ -645,7 +743,7 @@ public function getFunctionEntry(): string {
}
}

private function getFlagsAsString(): string
private function getFlagsAsArginfoString(): string
{
$flags = "ZEND_ACC_PUBLIC";
if ($this->flags & Class_::MODIFIER_PROTECTED) {
Expand Down Expand Up @@ -705,6 +803,8 @@ class FileInfo {
public $declarationPrefix = "";
/** @var bool */
public $generateLegacyArginfo = false;
/** @var bool */
public $generateMethodsynopses = false;

/**
* @return iterable<FuncInfo>
Expand Down Expand Up @@ -1042,6 +1142,8 @@ protected function pName_FullyQualified(Name\FullyQualified $node) {
$fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : "";
} else if ($tag->name === 'generate-legacy-arginfo') {
$fileInfo->generateLegacyArginfo = true;
} else if ($tag->name === 'generate-methodsynopses') {
$fileInfo->generateMethodsynopses = true;
}
}
}
Expand Down Expand Up @@ -1107,14 +1209,14 @@ function funcInfoToCode(FuncInfo $funcInfo): string {
"\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n",
$argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name,
$simpleArgType->toTypeCode(), $argType->isNullable(),
$argInfo->hasDefaultValue() ? ", " . $argInfo->getDefaultValueString() : ""
$argInfo->hasDefaultValue() ? ", " . $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->hasDefaultValue() ? ", " . $argInfo->getDefaultValueString() : ""
$argInfo->hasDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : ""
);
}
} else if (null !== $representableType = $argType->tryToRepresentableType()) {
Expand All @@ -1124,14 +1226,14 @@ function funcInfoToCode(FuncInfo $funcInfo): string {
$argKind, $argInfo->getSendByString(), $argInfo->name,
$representableType->classType->toEscapedName(),
$representableType->toTypeMask(),
$argInfo->getDefaultValueString()
$argInfo->getDefaultValueAsArginfoString()
);
} else {
$code .= sprintf(
"\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n",
$argKind, $argInfo->getSendByString(), $argInfo->name,
$representableType->toTypeMask(),
$argInfo->getDefaultValueString()
$argInfo->getDefaultValueAsArginfoString()
);
}
} else {
Expand All @@ -1141,7 +1243,7 @@ function funcInfoToCode(FuncInfo $funcInfo): string {
$code .= sprintf(
"\tZEND_%s_INFO%s(%s, %s%s)\n",
$argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name,
$argInfo->hasDefaultValue() ? ", " . $argInfo->getDefaultValueString() : ""
$argInfo->hasDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : ""
);
}
}
Expand Down Expand Up @@ -1253,6 +1355,87 @@ function generateFunctionEntries(?Name $className, array $funcInfos): string {
return $code;
}


/**
* @return array<string, string>
*/
function generateMethodsynopses(FileInfo $fileInfo): array {
$result = [];

/** @var FuncInfo $funcInfo */
foreach ($fileInfo->getAllFuncInfos() as $funcInfo) {
$doc = new DOMDocument();
$doc->appendChild($doc->createElement('methodsynopses'));

$comment = $doc->createComment(" {$funcInfo->name} ");
$doc->documentElement->appendChild($comment);
$doc->documentElement->appendChild(generateMethodsynopsis($doc, $funcInfo));

$xml = $doc->saveXML();
$xml = str_replace('<!--', "\n\n <!--", $xml);
$xml = str_replace('<methodsynopsis>', "\n <methodsynopsis>\n ", $xml);
$xml = str_replace('<methodparam', "\n <methodparam", $xml);
$xml = str_replace('<void/>', "\n <void/>", $xml);
$xml = str_replace('</methodsynopsis>', "\n </methodsynopsis>", $xml);
$xml = str_replace('</methodsynopses>', "\n\n</methodsynopses>", $xml);

$result[$funcInfo->name->getMethodsynopsisFilename() . ".xml"] = $xml;
}

return $result;
}

function generateMethodsynopsis(DOMDocument $doc, FuncInfo $funcInfo): DOMElement {
$methodsynopsis = $doc->createElement('methodsynopsis');
$typeString = $funcInfo->return->getTypeAsMethodsynopsisString();

foreach ($funcInfo->getModifierNames() as $modifierString) {
$modifierElement = $doc->createElement('modifier', $modifierString);
$methodsynopsis->appendChild($modifierElement);
}

$type = $doc->createElement('type', $typeString);
$methodsynopsis->appendChild($type);

$methodname = $doc->createElement('methodname', $funcInfo->name->__toString());
$methodsynopsis->appendChild($methodname);
if (empty($funcInfo->args)) {
$void = $doc->createElement('void');
$methodsynopsis->appendChild($void);
} else {
foreach ($funcInfo->args as $arg) {
$methodparam = $doc->createElement('methodparam');
if ($arg->defaultValue !== null) {
$choice = $doc->createAttribute('choice');
$choice->value = "opt";
$methodparam->appendChild($choice);
}
$methodsynopsis->appendChild($methodparam);
$type = $doc->createElement('type', $arg->getTypeAsMethodsynopsisString());
$methodparam->appendChild($type);
$parameter = $doc->createElement('parameter', $arg->name);
if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) {
$role = $doc->createAttribute('role');
$role->value = "reference";
$parameter->appendChild($role);
}
$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);
}
}
}
return $methodsynopsis;
}

function installPhpParser(string $version, string $phpParserDir) {
$lockFile = __DIR__ . "/PHP-Parser-install-lock";
$lockFd = fopen($lockFile, 'w+');
Expand Down Expand Up @@ -1318,16 +1501,17 @@ function initPhpParser() {
}

$optind = null;
$options = getopt("fh", ["force-regeneration", "parameter-stats", "help", "verify"], $optind);
$options = getopt("fh", ["force-regeneration", "parameter-stats", "help", "verify", "generate-methodsynopses"], $optind);

$context = new Context;
$printParameterStats = isset($options["parameter-stats"]);
$verify = isset($options["verify"]);
$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]);
$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify;
$context->generateMethodsynopses = isset($options["generate-methodsynopses"]);

if (isset($options["h"]) || isset($options["help"])) {
die("\nusage: gen-stub.php [ -f | --force-regeneration ] [ --parameter-stats ] [ --verify ] [ -h | --help ] [ name.stub.php | directory ]\n\n");
die("\nusage: gen-stub.php [ -f | --force-regeneration ] [ --parameter-stats ] [ --verify ] [ --generate-methodsynopses ] [ -h | --help ] [ name.stub.php | directory ]\n\n");
}

$fileInfos = [];
Expand Down
5 changes: 4 additions & 1 deletion ext/bcmath/bcmath.stub.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?php

/** @generate-function-entries */
/**
* @generate-function-entries
* @generate-methodsynopses
*/

function bcadd(string $num1, string $num2, ?int $scale = null): string {}

Expand Down
2 changes: 1 addition & 1 deletion ext/bcmath/bcmath_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 0c1e2a6163a5fc0f42bf79bbc530af7c5fd77074 */
* Stub hash: b82da40db260988d9043c5dc262966f4d64e32bc */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_bcadd, 0, 2, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, num1, IS_STRING, 0)
Expand Down
Loading

0 comments on commit fca45e9

Please sign in to comment.