diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 2fabe604..7ed87a33 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -2144,6 +2144,19 @@ class CompilerBase implements PropertyAccessContext $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 = ''; // 返回值的表达式是一个类的对象 $objectClass = $this->detectDeclaredClassOfExpr($v->expr); diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 8fd214b4..aef054cc 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -103,7 +103,8 @@ trait CallArgumentGenerator continue; } $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); diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 66dbb2e2..fc8ea15b 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -19,6 +19,87 @@ use PhpParser\NodeAbstract; 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 { $check = []; diff --git a/src/Translator.php b/src/Translator.php index 52b80885..98de0baf 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3176,7 +3176,15 @@ CODE; $cppCode .= $this->getIndent() . Type::ARRAY . ' ' . $var . ';' . PHP_EOL; $cppCode .= $this->getIndent() . 'for (uint32_t i = ' . $k . '; i < php::getCallArgNum(); i++) {' . PHP_EOL; $this->indentLevel++; - $cppCode .= $this->getIndent() . $var . '.append(php::getCallArg(i));' . PHP_EOL; + 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; + } $this->indentLevel--; $cppCode .= '}' . PHP_EOL; $cppCode .= $this->genExtraNamedVariadicArgs($var); @@ -3200,7 +3208,17 @@ CODE; } $cppType = $this->getDefaultArgumentType($argInfo); $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)); } else { $expr = $this->convertExprFromType($argInfo->type, $argExpr); diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 1d08660c..89fe0ca7 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -139,7 +139,12 @@ trait NativeTypeCompatibilityTrait 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); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); @@ -180,6 +185,20 @@ trait NativeTypeCompatibilityTrait $expr = $this->parseOrderedArg($arg); $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); if ($argInfo->type === Type::VAR && $this->isVarExpr($arg->value)) { @@ -215,4 +234,3 @@ trait NativeTypeCompatibilityTrait } } - diff --git a/tests/compiler/strict_types/scalar-dynamic-boundaries.phpt b/tests/compiler/strict_types/scalar-dynamic-boundaries.phpt new file mode 100644 index 00000000..9a1ce6bc --- /dev/null +++ b/tests/compiler/strict_types/scalar-dynamic-boundaries.phpt @@ -0,0 +1,71 @@ +--TEST-- +strict scalar declarations validate dynamic parameter and return values +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- + +--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)