[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