diff options
author | Máté Kocsis <kocsismate@woohoolabs.com> | 2020-10-13 20:03:00 +0200 |
---|---|---|
committer | Máté Kocsis <kocsismate@woohoolabs.com> | 2020-10-16 10:56:33 +0200 |
commit | d6264b09665c20ee73d7a1017db0730f9a7c7973 (patch) | |
tree | bb465d70db16df73ce28bcc1fadab1a57dd92c02 /build/gen_stub.php | |
parent | 3841ea338ef2508d467efef9ec45d03162ba695d (diff) | |
download | php-git-d6264b09665c20ee73d7a1017db0730f9a7c7973.tar.gz |
Verify parameter names of function aliases
Closes GH-6335
Diffstat (limited to 'build/gen_stub.php')
-rwxr-xr-x | build/gen_stub.php | 211 |
1 files changed, 185 insertions, 26 deletions
diff --git a/build/gen_stub.php b/build/gen_stub.php index e0dd8a2e24..8e7d915452 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -12,7 +12,12 @@ use PhpParser\PrettyPrinterAbstract; error_reporting(E_ALL); -function processDirectory(string $dir, Context $context) { +/** + * @return FileInfo[] + */ +function processDirectory(string $dir, Context $context): array { + $fileInfos = []; + $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY @@ -20,12 +25,17 @@ function processDirectory(string $dir, Context $context) { foreach ($it as $file) { $pathName = $file->getPathName(); if (preg_match('/\.stub\.php$/', $pathName)) { - processStubFile($pathName, $context); + $fileInfo = processStubFile($pathName, $context); + if ($fileInfo) { + $fileInfos[] = $fileInfo; + } } } + + return $fileInfos; } -function processStubFile(string $stubFile, Context $context) { +function processStubFile(string $stubFile, Context $context): ?FileInfo { try { if (!file_exists($stubFile)) { throw new Exception("File $stubFile does not exist"); @@ -37,15 +47,15 @@ function processStubFile(string $stubFile, Context $context) { $stubCode = file_get_contents($stubFile); $stubHash = computeStubHash($stubCode); $oldStubHash = extractStubHash($arginfoFile); - if ($stubHash === $oldStubHash && $context->forceRegeneration === false) { + if ($stubHash === $oldStubHash && !$context->forceParse) { /* Stub file did not change, do not regenerate. */ - return; + return null; } initPhpParser(); $fileInfo = parseStubFile($stubCode); $arginfoCode = generateArgInfoCode($fileInfo, $stubHash); - if (file_put_contents($arginfoFile, $arginfoCode)) { + if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) { echo "Saved $arginfoFile\n"; } @@ -54,20 +64,12 @@ function processStubFile(string $stubFile, Context $context) { $funcInfo->discardInfoForOldPhpVersions(); } $arginfoCode = generateArgInfoCode($fileInfo, $stubHash); - if (file_put_contents($legacyFile, $arginfoCode)) { + if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) { echo "Saved $legacyFile\n"; } } - // Collect parameter name statistics. - foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { - foreach ($funcInfo->args as $argInfo) { - if (!isset($context->parameterStats[$argInfo->name])) { - $context->parameterStats[$argInfo->name] = 0; - } - $context->parameterStats[$argInfo->name]++; - } - } + return $fileInfo; } catch (Exception $e) { echo "In $stubFile:\n{$e->getMessage()}\n"; exit(1); @@ -93,9 +95,9 @@ function extractStubHash(string $arginfoFile): ?string { class Context { /** @var bool */ + public $forceParse = false; + /** @var bool */ public $forceRegeneration = false; - /** @var array */ - public $parameterStats = []; } class SimpleType { @@ -358,6 +360,8 @@ interface FunctionOrMethodName { public function getArgInfoName(): string; public function __toString(): string; public function isMagicMethod(): bool; + public function isMethod(): bool; + public function isConstructor(): bool; } class FunctionName implements FunctionOrMethodName { @@ -402,6 +406,14 @@ class FunctionName implements FunctionOrMethodName { public function isMagicMethod(): bool { return false; } + + public function isMethod(): bool { + return false; + } + + public function isConstructor(): bool { + return false; + } } class MethodName implements FunctionOrMethodName { @@ -434,6 +446,14 @@ class MethodName implements FunctionOrMethodName { public function isMagicMethod(): bool { return strpos($this->methodName, '__') === 0; } + + public function isMethod(): bool { + return true; + } + + public function isConstructor(): bool { + return $this->methodName === "__construct"; + } } class ReturnInfo { @@ -457,6 +477,8 @@ class FuncInfo { /** @var FunctionOrMethodName */ public $name; /** @var int */ + public $classFlags; + /** @var int */ public $flags; /** @var string|null */ public $aliasType; @@ -464,6 +486,8 @@ class FuncInfo { public $alias; /** @var bool */ public $isDeprecated; + /** @var bool */ + public $verify; /** @var ArgInfo[] */ public $args; /** @var ReturnInfo */ @@ -475,26 +499,45 @@ class FuncInfo { 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(); + } + public function equalsApartFromName(FuncInfo $other): bool { if (count($this->args) !== count($other->args)) { return false; @@ -732,6 +775,7 @@ function parseDocComment(DocComment $comment): array { function parseFunctionLike( PrettyPrinterAbstract $prettyPrinter, FunctionOrMethodName $name, + int $classFlags, int $flags, Node\FunctionLike $func, ?string $cond @@ -741,6 +785,7 @@ function parseFunctionLike( $aliasType = null; $alias = null; $isDeprecated = false; + $verify = true; $haveDocReturnType = false; $docParamTypes = []; @@ -763,6 +808,8 @@ function parseFunctionLike( } } else if ($tag->name === 'deprecated') { $isDeprecated = true; + } else if ($tag->name === 'no-verify') { + $verify = false; } else if ($tag->name === 'return') { $haveDocReturnType = true; } else if ($tag->name === 'param') { @@ -843,10 +890,12 @@ function parseFunctionLike( return new FuncInfo( $name, + $classFlags, $flags, $aliasType, $alias, $isDeprecated, + $verify, $args, $return, $numRequiredArgs, @@ -917,6 +966,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $prettyPrinter, new FunctionName($stmt->namespacedName), 0, + 0, $stmt, $cond ); @@ -936,6 +986,11 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac 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; @@ -948,6 +1003,7 @@ function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstrac $methodInfos[] = parseFunctionLike( $prettyPrinter, new MethodName($className, $classStmt->name->toString()), + $classFlags, $flags, $classStmt, $cond @@ -1262,29 +1318,132 @@ function initPhpParser() { } $optind = null; -$options = getopt("fh", ["force-regeneration", "parameter-stats", "help"], $optind); +$options = getopt("fh", ["force-regeneration", "parameter-stats", "help", "verify"], $optind); $context = new Context; $printParameterStats = isset($options["parameter-stats"]); -$context->forceRegeneration = - isset($options["f"]) || isset($options["force-regeneration"]) || $printParameterStats; +$verify = isset($options["verify"]); +$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]); +$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify; if (isset($options["h"]) || isset($options["help"])) { - die("\nusage: gen-stub.php [ -f | --force-regeneration ] [ --parameter-stats ] [ -h | --help ] [ name.stub.php | directory ]\n\n"); + die("\nusage: gen-stub.php [ -f | --force-regeneration ] [ --parameter-stats ] [ --verify ] [ -h | --help ] [ name.stub.php | directory ]\n\n"); } +$fileInfos = []; $location = $argv[$optind] ?? "."; if (is_file($location)) { // Generate single file. - processStubFile($location, $context); + $fileInfo = processStubFile($location, $context); + if ($fileInfo) { + $fileInfos[] = $fileInfo; + } } else if (is_dir($location)) { - processDirectory($location, $context); + $fileInfos = processDirectory($location, $context); } else { echo "$location is neither a file nor a directory.\n"; exit(1); } if ($printParameterStats) { - arsort($context->parameterStats); - echo json_encode($context->parameterStats, JSON_PRETTY_PRINT), "\n"; + $parameterStats = []; + + foreach ($fileInfos as $fileInfo) { + foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { + foreach ($funcInfo->args as $argInfo) { + if (!isset($context->parameterStats[$argInfo->name])) { + $parameterStats[$argInfo->name] = 0; + } + $parameterStats[$argInfo->name]++; + } + } + } + + arsort($parameterStats); + echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n"; +} + +if ($verify) { + $errors = []; + $funcMap = []; + $aliases = []; + + foreach ($fileInfos as $fileInfo) { + foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { + /** @var FuncInfo $funcInfo */ + $funcMap[$funcInfo->name->__toString()] = $funcInfo; + + if ($funcInfo->aliasType === "alias") { + $aliases[] = $funcInfo; + } + } + } + + foreach ($aliases 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) { + assert($aliasArg !== 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); + } } |