[WIP]支持函数参数的类型检查 #44
Closed
NathanFreeman
wants to merge 2 commits from feat/params-type-check into master
4 changed files with 447 additions and 13 deletions
@ -0,0 +1,57 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
namespace TypePhp\Generator\ParamTypes; |
||||
|
||||
use TypePhp\Entity\ArgInfo; |
||||
|
||||
final class ParamParser |
||||
{ |
||||
/** |
||||
* ZEND_PARSE_PARAMETERS_NONE cannot be used here, because it hardcodes a return statement, |
||||
* which would result in a type mismatch. |
||||
*/ |
||||
private const string NONE_PARAMS_TEMPLATE = <<<EOL |
||||
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 0) |
||||
ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred()); |
||||
EOL; |
||||
|
||||
/** |
||||
* @param array<ArgInfo> $argInfos |
||||
* @return string |
||||
*/ |
||||
public function createZendParamTypeCheck(array $argInfos): string |
||||
{ |
||||
// Return the default macro if no parameter information is available. |
||||
if (empty($argInfos)) { |
||||
return sprintf("do { %s } while(false);", self::NONE_PARAMS_TEMPLATE); |
||||
} |
||||
|
||||
[$minNumArgs, $maxNumArgs] = $this->getRequiredAndAllParamCounts($argInfos); |
||||
$zendVariables = (new ZendVariables())->createZendVariables($argInfos); |
||||
$zendParamMarcos = (new ZendParamMacros())->createZendParamMacros($argInfos, $minNumArgs, $maxNumArgs); |
||||
|
||||
return sprintf( |
||||
"do { %s %s } while(false);", |
||||
implode(PHP_EOL, $zendVariables), |
||||
implode(PHP_EOL, $zendParamMarcos)); |
||||
} |
||||
|
||||
/** |
||||
* Calculate the count of required parameters and total parameters. |
||||
* @param array<ArgInfo> $argInfos |
||||
* @return array<int> |
||||
*/ |
||||
private function getRequiredAndAllParamCounts(array $argInfos): array |
||||
{ |
||||
$argCount = count($argInfos); |
||||
$lastIndex = $argCount - 1; |
||||
|
||||
if ($argCount > 0 && $argInfos[$lastIndex]->variadic) { |
||||
return [$lastIndex, -1]; |
||||
} |
||||
|
||||
$requiredParamCount = count(array_filter($argInfos, fn($argInfo) => $argInfo->defaultValue === null)); |
||||
return [$requiredParamCount, $argCount]; |
||||
} |
||||
} |
||||
@ -0,0 +1,198 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
namespace TypePhp\Generator\ParamTypes; |
||||
|
||||
use PhpParser\Node\Identifier; |
||||
use PhpParser\Node\NullableType; |
||||
use TypePhp\Entity\ArgInfo; |
||||
use TypePhp\Type; |
||||
|
||||
final class ZendParamMacros |
||||
{ |
||||
|
||||
/** |
||||
* Generate a macro in the extension to validate/check the input parameters. |
||||
* @param array<ArgInfo> $argInfos |
||||
* @param int $minNumArgs |
||||
* @param int $maxNumArgs |
||||
* @return array<string> |
||||
*/ |
||||
public function createZendParamMacros(array $argInfos, int $minNumArgs, int $maxNumArgs): array |
||||
{ |
||||
$marcos[] = $this->genStartParseCode($minNumArgs, $maxNumArgs); |
||||
foreach ($argInfos as $index => $argInfo) { |
||||
if ($maxNumArgs > $minNumArgs && $index == $minNumArgs) { |
||||
$marcos[] = $this->genOptionalParseCode(); |
||||
} |
||||
|
||||
$marcos[] = match ($argInfo->type) { |
||||
Type::BOOL => $this->parseBool($argInfo), |
||||
Type::INT => $this->parseInt($argInfo), |
||||
Type::FLOAT => $this->parseFloat($argInfo), |
||||
Type::ARRAY => $this->parseArray($argInfo), |
||||
Type::STR => $this->parseString($argInfo), |
||||
Type::REF => $this->parseReference($argInfo), |
||||
Type::RESOURCE, Type::STREAM => $this->parseResource($argInfo), |
||||
Type::OBJECT => $this->parseObject($argInfo, $argInfo->class), |
||||
default => $this->parseVar($argInfo) |
||||
}; |
||||
} |
||||
|
||||
$marcos[] = $this->genEndParseCode(); |
||||
return $marcos; |
||||
} |
||||
|
||||
/** |
||||
* ZEND_PARAM_PARSE start |
||||
* @param int $minNumArgs |
||||
* @param int $maxNumArgs |
||||
* @return string |
||||
*/ |
||||
private function genStartParseCode(int $minNumArgs, int $maxNumArgs): string |
||||
{ |
||||
return sprintf("ZEND_PARSE_PARAMETERS_START(%d, %d)", $minNumArgs, $maxNumArgs); |
||||
} |
||||
|
||||
/** |
||||
* ZEND_PARAM_PARSE end |
||||
* @return string |
||||
*/ |
||||
private function genEndParseCode(): string |
||||
{ |
||||
return "ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred());"; |
||||
} |
||||
|
||||
/** |
||||
* Z_PARAM_OPTIONAL |
||||
* @return string |
||||
*/ |
||||
private function genOptionalParseCode(): string |
||||
{ |
||||
return "Z_PARAM_OPTIONAL"; |
||||
} |
||||
|
||||
private function parseBool(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return $argInfo->nullable |
||||
? sprintf("Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", $name, $name) |
||||
: "Z_PARAM_BOOL($name)"; |
||||
} |
||||
|
||||
private function parseInt(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return $argInfo->nullable |
||||
? sprintf("Z_PARAM_LONG_OR_NULL(%s, %s_is_null)", $name, $name) |
||||
: "Z_PARAM_LONG($name)"; |
||||
} |
||||
|
||||
private function parseFloat(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return $argInfo->nullable |
||||
? sprintf("Z_PARAM_DOUBLE_OR_NULL(%s, %s_is_null)", $name, $name) |
||||
: "Z_PARAM_DOUBLE($name)"; |
||||
} |
||||
|
||||
private function parseArray(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return $argInfo->nullable ? "Z_PARAM_ARRAY_OR_NULL($name)" : "Z_PARAM_ARRAY($name)"; |
||||
} |
||||
|
||||
private function parseString(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return $argInfo->nullable ? "Z_PARAM_STR_OR_NULL($name)" : "Z_PARAM_STR($name)"; |
||||
} |
||||
|
||||
private function parseArgs(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return sprintf("Z_PARAM_VARIADIC(*, %s_argv, %s_argc)", $name, $name); |
||||
} |
||||
|
||||
private function parseReference(ArgInfo $argInfo): string |
||||
{ |
||||
return "Z_PARAM_ZVAL($argInfo->name)"; |
||||
} |
||||
|
||||
private function parseResource(ArgInfo $argInfo): string |
||||
{ |
||||
return "Z_PARAM_RESOURCE($argInfo->name)"; |
||||
} |
||||
|
||||
private function parseObject(ArgInfo $argInfo, string $className): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
if ($className) { |
||||
return $argInfo->nullable |
||||
? sprintf("Z_PARAM_OBJECT_OF_CLASS_OR_NULL(%s_obj, %s_ce)", $name, $name) |
||||
: sprintf("Z_PARAM_OBJECT_OF_CLASS(%s_obj, %s_ce)", $name, $name); |
||||
} |
||||
|
||||
return $argInfo->nullable ? "Z_PARAM_OBJECT_OR_NULL($name)" : "Z_PARAM_OBJECT($name)"; |
||||
} |
||||
|
||||
/** |
||||
* The compiler does not currently support the callable type. |
||||
* For now, we'll implement the method first and fill in the callable support later. |
||||
* @param \TypePhp\Entity\ArgInfo $argInfo |
||||
* @return string |
||||
*/ |
||||
private function parseCallable(ArgInfo $argInfo): string |
||||
{ |
||||
$name = $argInfo->name; |
||||
return $argInfo->nullable |
||||
? sprintf("Z_PARAM_FUNC_OR_NULL(%s_fci, %s_fcc)", $name, $name) |
||||
: sprintf("Z_PARAM_FUNC(%s_fci, %s_fcc)", $name, $name); |
||||
} |
||||
|
||||
/** |
||||
* For multiple types, we only handle plain types, mixed, and nullable variables. |
||||
* The remaining UnionType and IntersectionType are too complex to be processed via macros. |
||||
* @param \TypePhp\Entity\ArgInfo $argInfo |
||||
* @return string |
||||
*/ |
||||
private function parseVar(ArgInfo $argInfo): string |
||||
{ |
||||
if ($argInfo->variadic) { |
||||
return $this->parseArgs($argInfo); |
||||
} |
||||
|
||||
if ($argInfo->byRef) { |
||||
return $this->parseArgs($argInfo); |
||||
} |
||||
|
||||
$name = $argInfo->name; |
||||
if (!$argInfo->typeNode instanceof NullableType) { |
||||
return $argInfo->nullable ? "Z_PARAM_ZVAL_OR_NULL($name)" : "Z_PARAM_ZVAL($name)"; |
||||
} |
||||
|
||||
/** |
||||
* Nullable only contains Identifier, FullyQualified, and Relative. |
||||
* The Relative option is not supported by the compiler. |
||||
*/ |
||||
$type = $argInfo->typeNode->type; |
||||
if ($type instanceof Identifier) { |
||||
return match ($type->name) { |
||||
"int" => $this->parseInt($argInfo), |
||||
"string" => $this->parseString($argInfo), |
||||
"array" => $this->parseArray($argInfo), |
||||
"bool" => $this->parseBool($argInfo), |
||||
"float" => $this->parseFloat($argInfo), |
||||
"object" => $this->parseObject($argInfo, $argInfo->class), |
||||
/** |
||||
* A default case is not strictly required here, since the PHP-supported types are already listed above. |
||||
* However, it is included anyway to keep the code robust in case new types are introduced in the future. |
||||
*/ |
||||
default => "Z_PARAM_ZVAL_OR_NULL($name)" |
||||
}; |
||||
} |
||||
|
||||
// FullyQualified |
||||
return $this->parseObject($argInfo, $type->toString()); |
||||
} |
||||
} |
||||
@ -0,0 +1,140 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
namespace TypePhp\Generator\ParamTypes; |
||||
|
||||
use PhpParser\Node\Identifier; |
||||
use PhpParser\Node\NullableType; |
||||
use TypePhp\Entity\ArgInfo; |
||||
use TypePhp\Type; |
||||
|
||||
final class ZendVariables |
||||
{ |
||||
/** |
||||
* @param array<ArgInfo> $argInfos |
||||
* @return array |
||||
*/ |
||||
public function createZendVariables(array $argInfos): array |
||||
{ |
||||
$variables = []; |
||||
foreach ($argInfos as $argInfo) { |
||||
$variables[] = match ($argInfo->type) { |
||||
Type::BOOL => $this->genBoolVariable($argInfo->name), |
||||
Type::INT => $this->genIntVariable($argInfo->name), |
||||
Type::FLOAT => $this->genFloatVariable($argInfo->name), |
||||
Type::ARRAY => $this->genArrayVariable($argInfo->name), |
||||
Type::STR => $this->genStringVariable($argInfo->name), |
||||
Type::RESOURCE, Type::STREAM => $this->genResourceVariable($argInfo->name), |
||||
Type::OBJECT => $this->genObjectVariable($argInfo->name, $argInfo->class), |
||||
default => $this->genVarVariable($argInfo), |
||||
}; |
||||
} |
||||
|
||||
return $variables; |
||||
} |
||||
|
||||
private function genBoolVariable(string $name): string |
||||
{ |
||||
return sprintf("zend_bool %s = 0; zend_bool %s_is_null = false;", $name, $name); |
||||
} |
||||
|
||||
private function genIntVariable(string $name): string |
||||
{ |
||||
return sprintf("zend_long %s = 0; zend_bool %s_is_null = false;", $name, $name); |
||||
} |
||||
|
||||
private function genFloatVariable(string $name): string |
||||
{ |
||||
return sprintf("double %s = 0; zend_bool %s_is_null = false;", $name, $name); |
||||
} |
||||
|
||||
private function genArrayVariable(string $name): string |
||||
{ |
||||
return sprintf("zval *%s = nullptr;", $name); |
||||
} |
||||
|
||||
private function genStringVariable(string $name): string |
||||
{ |
||||
return sprintf("zend_string *%s = nullptr;", $name); |
||||
} |
||||
|
||||
private function genArgsVariable(string $name): string |
||||
{ |
||||
return sprintf("zval *%s_argv = nullptr; int %s_argc = 0;", $name, $name); |
||||
} |
||||
|
||||
private function genReferenceVariable(string $name): string |
||||
{ |
||||
return sprintf("zval *%s = nullptr;", $name); |
||||
} |
||||
|
||||
private function genResourceVariable(string $name): string |
||||
{ |
||||
return sprintf("zval *%s = nullptr;", $name); |
||||
} |
||||
|
||||
private function genObjectVariable(string $name, string $className): string |
||||
{ |
||||
if ($className) { |
||||
return sprintf(<<<EOL |
||||
zend_string *%s_search = zend_string_init("%s", %d, 0); |
||||
zend_class_entry *%s_ce = zend_lookup_class(%s_search); |
||||
zend_string_release(%s_search); |
||||
zval *%s_obj = nullptr; |
||||
EOL, |
||||
$name, str_replace("\\", "\\\\", $className), strlen($className), $name, $name, $name, $name); |
||||
} |
||||
|
||||
return sprintf("zval *%s = nullptr;", $name); |
||||
} |
||||
|
||||
/** |
||||
* The compiler does not currently support the callable type. |
||||
* For now, we'll implement the method first and fill in the callable support later. |
||||
* @param string $name |
||||
* @return string |
||||
*/ |
||||
private function genCallableVariable(string $name): string |
||||
{ |
||||
return sprintf("zend_fcall_info %s_fci; zend_fcall_info_cache %s_fcc;", $name, $name); |
||||
} |
||||
|
||||
private function genVarVariable(ArgInfo $argInfo): string |
||||
{ |
||||
if ($argInfo->variadic) { |
||||
return $this->genArgsVariable($argInfo->name); |
||||
} |
||||
|
||||
if ($argInfo->byRef) { |
||||
return $this->genReferenceVariable($argInfo->name); |
||||
} |
||||
|
||||
if (!$argInfo->typeNode instanceof NullableType) { |
||||
return sprintf("zval *%s = nullptr;", $argInfo->name); |
||||
} |
||||
|
||||
/** @var NullableType $typeNode */ |
||||
$typeNode = $argInfo->typeNode; |
||||
$type = $typeNode->type; |
||||
$name = $argInfo->name; |
||||
|
||||
/** |
||||
* Nullable only contains Identifier, FullyQualified, and Relative. |
||||
* The Relative option is not supported by the compiler. |
||||
*/ |
||||
if ($type instanceof Identifier) { |
||||
return match ($type->name) { |
||||
'bool' => $this->genBoolVariable($name), |
||||
'int' => $this->genIntVariable($name), |
||||
'string' => $this->genStringVariable($name), |
||||
'float' => $this->genFloatVariable($name), |
||||
'array' => $this->genArrayVariable($name), |
||||
'object' => $this->genObjectVariable($name, ''), |
||||
default => sprintf("zval *%s = nullptr;", $name), |
||||
}; |
||||
} |
||||
|
||||
// FullyQualified |
||||
return $this->genObjectVariable($name, $type->toString()); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue