feat(compiler): implement strict scalar type validation for parameters and returns

- Add strict scalar type checking for function parameters and return values
- Preserve zval types until declared boundaries are validated
- Generate type error exceptions with detailed argument information
- Support dynamic parameter validation in variadic and default argument contexts
- Add comprehensive test coverage for strict scalar type boundaries
- Introduce helper methods for scalar type condition generation and validation
pull/45/head
韩天峰 3 weeks ago
parent 6d7e68a9ff
commit ec298e66fe
  1. 13
      src/CompilerBase.php
  2. 3
      src/Generator/CallArgumentGenerator.php
  3. 81
      src/Generator/TypeCheckGenerator.php
  4. 20
      src/Translator.php
  5. 22
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  6. 71
      tests/compiler/strict_types/scalar-dynamic-boundaries.phpt

@ -2144,6 +2144,19 @@ class CompilerBase implements PropertyAccessContext
$returnType = Type::VAR; $returnType = Type::VAR;
} }
if (!$this->context->inClosure
&& ($type === Type::VAR || $type === Type::REF)
&& $this->isStrictScalarType($returnType)) {
// Keep the zval type until the declared return boundary has been
// checked. Converting first would silently coerce invalid values.
$tmpVar = $this->addTmpVar(Type::VAR);
$code = $tmpVar . ' = (' . $expr . ');' . PHP_EOL;
$code .= $this->genStrictScalarReturnCheck($tmpVar, $returnType);
$code .= $this->getIndent() . 'return '
. $this->convertExprType($tmpVar, $returnType, Type::VAR) . ';';
return $code;
}
$returnObjectCheckClass = ''; $returnObjectCheckClass = '';
// 返回值的表达式是一个类的对象 // 返回值的表达式是一个类的对象
$objectClass = $this->detectDeclaredClassOfExpr($v->expr); $objectClass = $this->detectDeclaredClassOfExpr($v->expr);

@ -103,7 +103,8 @@ trait CallArgumentGenerator
continue; continue;
} }
$argInfo = $this->getArgInfo($arg, $nativeFunc, $i); $argInfo = $this->getArgInfo($arg, $nativeFunc, $i);
$argList[] = $this->getTypeConvertedArg($arg, $argInfo); $callableName = $functionDef->displayName ?: $functionDef->getNamespacedName();
$argList[] = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $i);
} }
return implode(', ', $argList); return implode(', ', $argList);

@ -19,6 +19,87 @@ use PhpParser\NodeAbstract;
trait TypeCheckGenerator trait TypeCheckGenerator
{ {
protected function isStrictScalarType(string $type): bool
{
return in_array($type, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR], true);
}
protected function strictScalarTypeName(string $type): string
{
return match ($type) {
Type::INT => 'int',
Type::FLOAT => 'float',
Type::BOOL => 'bool',
Type::STR => 'string',
default => throw new \LogicException('Not a strict scalar type: ' . $type),
};
}
protected function genStrictScalarCondition(string $valueExpr, string $type): string
{
return match ($type) {
Type::INT => $valueExpr . '.isInt()',
// PHP permits int values at a float boundary even in strict mode.
Type::FLOAT => '(' . $valueExpr . '.isFloat() || ' . $valueExpr . '.isInt())',
Type::BOOL => $valueExpr . '.isBool()',
Type::STR => $valueExpr . '.isString()',
default => throw new \LogicException('Not a strict scalar type: ' . $type),
};
}
protected function genStrictScalarParamCheck(
ArgInfo $argInfo,
string $valueExpr,
string $callableName,
string $argNoExpr
): string {
if (!$this->isStrictScalarType($argInfo->type)) {
return '';
}
$paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
$format = $this->genCharPtr($callableName . '(): Argument #', true)
. ' ZEND_LONG_FMT '
. $this->genCharPtr(
' ($' . $paramName . ') must be of type ' . $this->strictScalarTypeName($argInfo->type)
. ', %s given',
true
);
$throwExpr = 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', '
. $argNoExpr . ', ' . $valueExpr . '.typeStr())';
$code = $this->getIndent() . 'if (UNEXPECTED(!('
. $this->genStrictScalarCondition($valueExpr, $argInfo->type) . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . $throwExpr . ';' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}
protected function genStrictScalarReturnCheck(string $valueExpr, string $returnType): string
{
if (!$this->isStrictScalarType($returnType)) {
return '';
}
$fnName = $this->getTypeCheckCallableName();
$format = $this->genCharPtr(
$fnName . '(): Return value must be of type ' . $this->strictScalarTypeName($returnType)
. ', %s returned',
true
);
$code = $this->getIndent() . 'if (UNEXPECTED(!('
. $this->genStrictScalarCondition($valueExpr, $returnType) . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_type_error, 0, '
. $format . ', ' . $valueExpr . '.typeStr());' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}
protected function buildTypeCheckFromNode(NodeAbstract $typeNode): array protected function buildTypeCheckFromNode(NodeAbstract $typeNode): array
{ {
$check = []; $check = [];

@ -3176,7 +3176,15 @@ CODE;
$cppCode .= $this->getIndent() . Type::ARRAY . ' ' . $var . ';' . PHP_EOL; $cppCode .= $this->getIndent() . Type::ARRAY . ' ' . $var . ';' . PHP_EOL;
$cppCode .= $this->getIndent() . 'for (uint32_t i = ' . $k . '; i < php::getCallArgNum(); i++) {' . PHP_EOL; $cppCode .= $this->getIndent() . 'for (uint32_t i = ' . $k . '; i < php::getCallArgNum(); i++) {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
if ($this->isStrictScalarType($argInfo->type)) {
$rawVar = 'raw_' . $var;
$cppCode .= $this->getIndent() . Type::VAR . ' ' . $rawVar . ' = php::getCallArg(i);' . PHP_EOL;
$cppCode .= $this->genStrictScalarParamCheck($argInfo, $rawVar, $displayName, 'i + 1');
$cppCode .= $this->getIndent() . $var . '.append('
. $this->convertExprFromType($argInfo->type, $rawVar) . ');' . PHP_EOL;
} else {
$cppCode .= $this->getIndent() . $var . '.append(php::getCallArg(i));' . PHP_EOL; $cppCode .= $this->getIndent() . $var . '.append(php::getCallArg(i));' . PHP_EOL;
}
$this->indentLevel--; $this->indentLevel--;
$cppCode .= '}' . PHP_EOL; $cppCode .= '}' . PHP_EOL;
$cppCode .= $this->genExtraNamedVariadicArgs($var); $cppCode .= $this->genExtraNamedVariadicArgs($var);
@ -3200,7 +3208,17 @@ CODE;
} }
$cppType = $this->getDefaultArgumentType($argInfo); $cppType = $this->getDefaultArgumentType($argInfo);
$declaredClass = $argInfo->declaredClass ?: $argInfo->class; $declaredClass = $argInfo->declaredClass ?: $argInfo->class;
if ($argInfo->type === Type::OBJECT && $declaredClass !== '') { if ($this->isStrictScalarType($argInfo->type)) {
$rawVar = 'raw_' . $var;
$cppCode .= $this->getIndent() . Type::VAR . ' ' . $rawVar . ' = ' . $argExpr . ';' . PHP_EOL;
$cppCode .= $this->genStrictScalarParamCheck(
$argInfo,
$rawVar,
$displayName,
(string) ($k + 1)
);
$expr = $this->convertExprFromType($argInfo->type, $rawVar);
} elseif ($argInfo->type === Type::OBJECT && $declaredClass !== '') {
$expr = $this->convertObjectExpr($argExpr, $this->getClassEntryPtr($declaredClass)); $expr = $this->convertObjectExpr($argExpr, $this->getClassEntryPtr($declaredClass));
} else { } else {
$expr = $this->convertExprFromType($argInfo->type, $argExpr); $expr = $this->convertExprFromType($argInfo->type, $argExpr);

@ -139,7 +139,12 @@ trait NativeTypeCompatibilityTrait
return false; return false;
} }
protected function getTypeConvertedArg(Node\Arg $arg, ArgInfo $argInfo): string protected function getTypeConvertedArg(
Node\Arg $arg,
ArgInfo $argInfo,
string $callableName = '',
int $argIndex = 0
): string
{ {
$type = $this->detectTypeOfExpr($arg->value); $type = $this->detectTypeOfExpr($arg->value);
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
@ -180,6 +185,20 @@ trait NativeTypeCompatibilityTrait
$expr = $this->parseOrderedArg($arg); $expr = $this->parseOrderedArg($arg);
$expr = $this->materializeCallArgValue($arg->value, $expr); $expr = $this->materializeCallArgValue($arg->value, $expr);
if (($type === Type::VAR || $type === Type::REF) && $this->isStrictScalarType($argInfo->type)) {
// A native scalar ABI value has already lost its zval type. Preserve
// the dynamic value until strict_types validation has completed.
$tmpVar = $this->addTmpVar(Type::VAR);
$this->context->beforeStmtLines[] = $tmpVar . ' = (' . $expr . ');';
$this->context->beforeStmtLines[] = rtrim($this->genStrictScalarParamCheck(
$argInfo,
$tmpVar,
$callableName,
(string) ($argIndex + 1)
));
$expr = $tmpVar;
}
$this->checkVarAssignExpr($arg, $argInfo->type, $type); $this->checkVarAssignExpr($arg, $argInfo->type, $type);
if ($argInfo->type === Type::VAR && $this->isVarExpr($arg->value)) { if ($argInfo->type === Type::VAR && $this->isVarExpr($arg->value)) {
@ -215,4 +234,3 @@ trait NativeTypeCompatibilityTrait
} }
} }

@ -0,0 +1,71 @@
--TEST--
strict scalar declarations validate dynamic parameter and return values
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php
declare(strict_types=1);
function acceptsInt(int $value): int { return $value; }
function acceptsFloat(float $value): float { return $value; }
function acceptsBool(bool $value): bool { return $value; }
function acceptsString(string $value): string { return $value; }
function invalidIntReturn(): int { return json_decode('"bad"'); }
function invalidFloatReturn(): float { return json_decode('"bad"'); }
function invalidBoolReturn(): bool { return json_decode('1'); }
function invalidStringReturn(): string { return json_decode('1'); }
function widenedFloatReturn(): float { return json_decode('7'); }
function main(): void
{
$string = json_decode('"bad"');
$integer = json_decode('1');
try { acceptsInt($string); echo "int-param=accepted\n"; }
catch (TypeError $error) { echo "int-param=TypeError\n"; }
try { acceptsFloat($string); echo "float-param=accepted\n"; }
catch (TypeError $error) { echo "float-param=TypeError\n"; }
try { acceptsBool($integer); echo "bool-param=accepted\n"; }
catch (TypeError $error) { echo "bool-param=TypeError\n"; }
try { acceptsString($integer); echo "string-param=accepted\n"; }
catch (TypeError $error) { echo "string-param=TypeError\n"; }
try { invalidIntReturn(); echo "int-return=accepted\n"; }
catch (TypeError $error) { echo "int-return=TypeError\n"; }
try { invalidFloatReturn(); echo "float-return=accepted\n"; }
catch (TypeError $error) { echo "float-return=TypeError\n"; }
try { invalidBoolReturn(); echo "bool-return=accepted\n"; }
catch (TypeError $error) { echo "bool-return=TypeError\n"; }
try { invalidStringReturn(); echo "string-return=accepted\n"; }
catch (TypeError $error) { echo "string-return=TypeError\n"; }
$dynamicCall = 'acceptsInt';
try { $dynamicCall($string); echo "dynamic-param=accepted\n"; }
catch (TypeError $error) { echo "dynamic-param=TypeError\n"; }
var_dump(acceptsInt(json_decode('7')));
var_dump(acceptsFloat(json_decode('1.5')));
var_dump(acceptsFloat(json_decode('7')));
var_dump(acceptsBool(json_decode('true')));
var_dump(acceptsString(json_decode('"ok"')));
var_dump(widenedFloatReturn());
}
?>
--EXPECT--
int-param=TypeError
float-param=TypeError
bool-param=TypeError
string-param=TypeError
int-return=TypeError
float-return=TypeError
bool-return=TypeError
string-return=TypeError
dynamic-param=TypeError
int(7)
float(1.5)
float(7)
bool(true)
string(2) "ok"
float(7)
Loading…
Cancel
Save