fix(compiler): validate property default expressions

pull/24/head
韩天峰 1 month ago
parent 8c3cbd4dce
commit 82739d3d66
  1. 12
      phpunit/code/property-default-class-const-type.php
  2. 10
      phpunit/code/property-default-expression-type.php
  3. 10
      phpunit/code/property-default-false-for-true.php
  4. 10
      phpunit/code/property-default-true-for-false.php
  5. 32
      phpunit/src/ClassTest.php
  6. 79
      src/Preprocessor.php
  7. 10
      src/Translator.php
  8. 41
      src/gen_stub.php
  9. 53
      tests/compiler/object_property/default-expressions-inheritance.phpt

@ -0,0 +1,12 @@
<?php
class PropertyDefaultClassConstType
{
public int $value = self::DEFAULT_VALUE;
private const DEFAULT_VALUE = 'invalid';
}
function main(): void
{
}

@ -0,0 +1,10 @@
<?php
class PropertyDefaultExpressionType
{
public int $value = 1.5 + 2;
}
function main(): void
{
}

@ -0,0 +1,10 @@
<?php
class PropertyDefaultFalseForTrue
{
public true $value = false;
}
function main(): void
{
}

@ -0,0 +1,10 @@
<?php
class PropertyDefaultTrueForFalse
{
public false $value = true;
}
function main(): void
{
}

@ -232,4 +232,36 @@ class ClassTest extends \BaseTest
// 合法的默认值(含 int→float 协变、nullable、联合类型、mixed、常量)应通过检查。
$this->compile('property-default-valid.php');
}
public function testTrueDefaultForFalsePropertyFailsAtCompileTime()
{
$this->exec(
'Cannot use true as default value for property PropertyDefaultTrueForFalse::$value of type false',
'property-default-true-for-false.php'
);
}
public function testFalseDefaultForTruePropertyFailsAtCompileTime()
{
$this->exec(
'Cannot use false as default value for property PropertyDefaultFalseForTrue::$value of type true',
'property-default-false-for-true.php'
);
}
public function testClassConstantPropertyDefaultTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use string as default value for property PropertyDefaultClassConstType::$value of type int',
'property-default-class-const-type.php'
);
}
public function testExpressionPropertyDefaultTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use float as default value for property PropertyDefaultExpressionType::$value of type int',
'property-default-expression-type.php'
);
}
}

@ -21,6 +21,7 @@ use TypePhp\Exception\SyntaxError;
use TypePhp\Transform\PropertyHookLowering;
use TypePhp\Transform\Visitor;
use PhpParser\Modifiers;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\NullableType;
@ -620,12 +621,20 @@ class Preprocessor extends CompilerBase
}
$this->symbolDeclInFile[$fullClassNameLower] = $this->file;
// Property defaults may reference class constants declared later in the
// class body. Collect every constant first so default-value validation
// is independent of declaration order, matching PHP's class semantics.
foreach ($class->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\ClassConst) {
$this->parseClassConstDef($stmt);
}
}
$code = '';
foreach ($class->stmts as $v) {
$type = $v->getType();
switch ($type) {
case 'Stmt_ClassConst':
$this->parseClassConstDef($v);
break;
case 'Stmt_Property':
$this->parseClassPropertyDef($v);
@ -968,11 +977,16 @@ class Preprocessor extends CompilerBase
/**
* Determine the PHP value type of a constant expression used as a default
* value. Returns one of int/float/string/bool/array/null, or null when the
* type cannot be decided statically.
* value. Returns one of int/float/string/true/false/array/null, or null when
* the type cannot be decided statically.
*/
protected function detectDefaultValueType(NodeAbstract $node): ?string
protected function detectDefaultValueType(NodeAbstract $node, ?string $scopeClass = null, int $depth = 0): ?string
{
if ($depth > 16) {
return null;
}
$scopeClass ??= $this->getFullClassName();
switch ($node->getType()) {
case 'Scalar_Int':
return 'int';
@ -986,15 +1000,62 @@ class Preprocessor extends CompilerBase
return 'array';
case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus':
return $this->detectDefaultValueType($node->expr);
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
case 'Expr_ConstFetch':
return match (strtolower($node->name->toString())) {
'true', 'false' => 'bool',
'true' => 'true',
'false' => 'false',
'null' => 'null',
default => null,
};
case 'Expr_ClassConstFetch':
if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) {
return null;
}
$constName = $node->name->toString();
if (strcasecmp($constName, 'class') === 0) {
return 'string';
}
$className = $node->class->toString();
if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) {
$targetClass = $scopeClass;
} elseif (strcasecmp($className, 'parent') === 0) {
$targetClass = $this->getParentClass($scopeClass);
} else {
$targetClass = $this->getNamespacedClassName($className);
}
if ($targetClass === '' || !$this->hasClass($targetClass)) {
return null;
}
$targetDef = $this->getClass($targetClass);
if (!$targetDef->hasConstant($constName)) {
return null;
}
return $this->detectDefaultValueType(
$targetDef->getConstant($constName)->valueExpr,
$targetClass,
$depth + 1
);
default:
return null;
try {
$value = (new ConstExprEvaluator(
static function (Node\Expr $expr): never {
throw new \RuntimeException('Unresolved constant expression');
}
))->evaluateDirectly($node);
} catch (\Throwable) {
return null;
}
return match (true) {
is_int($value) => 'int',
is_float($value) => 'float',
is_string($value) => 'string',
$value === true => 'true',
$value === false => 'false',
is_array($value) => 'array',
$value === null => 'null',
default => null,
};
}
}
@ -1037,7 +1098,9 @@ class Preprocessor extends CompilerBase
'int' => ['int'],
'float', 'double' => ['float', 'int'], // int coerces to float
'string' => ['string'],
'bool', 'true', 'false' => ['bool'],
'bool' => ['true', 'false'],
'true' => ['true'],
'false' => ['false'],
'array' => ['array'],
'iterable' => ['array'],
'null' => ['null'],

@ -887,10 +887,13 @@ CODE;
. $property->arrayInitPlan->expr . ');' . PHP_EOL;
$code .= $this->wrapArrayInitPlan($property->arrayInitPlan, $statement);
} else {
$default = $property->type === Type::FLOAT
? $this->convertFloatExpr($property->default)
: $property->default;
$statement = 'php::setStaticProperty('
. $this->genCharPtr($classDef->getNamespacedName(false), true) . ', '
. $this->genCharPtr($property->name) . ', '
. 'php::Var(' . $property->default . '));' . PHP_EOL;
. 'php::Var(' . $default . '));' . PHP_EOL;
$code .= $statement;
}
}
@ -1643,8 +1646,11 @@ CODE;
// property table via zend_update_property. Each property is
// wrapped in its own block so the local `value` does not
// clash with siblings declared in the same create_object body.
$default = $property->type === Type::FLOAT
? $this->convertFloatExpr($property->default)
: $property->default;
$init = "do {\n";
$init .= "auto value = php::Var({$property->default});\n";
$init .= "auto value = php::Var({$default});\n";
$init .= 'zend_update_property(obj->ce, obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n";
$init .= "php::throwErrorIfOccurred();\n";
$init .= "} while (0);\n";

@ -2371,12 +2371,16 @@ class EvaluatedValue
if ($class === 'self') {
$constName = ClassInfo::$currentClass . "::" . $constName;
if (isset($allConstInfos[$constName])) {
return formatConstValue($allConstInfos[$constName]->getValue($allConstInfos)->value);
return $allConstInfos[$constName]->getValue($allConstInfos)->value;
} else {
return formatConstValue(getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $constName));
return normalizeConstExprValue(
getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $constName)
);
}
} else {
return formatConstValue(getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass));
return normalizeConstExprValue(
getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass)
);
}
} else {
$constName = $expr->name->__toString();
@ -2414,7 +2418,7 @@ class EvaluatedValue
if (isset($definedConstants[$constName])) {
$constValue = $definedConstants[$constName];
if (is_scalar($constValue)) {
return formatConstValue($constValue);
return $constValue;
}
}
@ -2508,11 +2512,15 @@ class EvaluatedValue
// fully qualified class name (already stored in $this->value).
return '"' . addcslashes($this->value, '\\') . '"';
}
return $this->value;
return '"' . getTranslator()->escapeString((string) $this->value) . '"';
} elseif ($this->expr instanceof Expr\ConstFetch) {
return getTranslator()->getConstValue($this->expr->name->toString());
} elseif (!($this->expr instanceof String_)) {
throw new Exception("Expression at line " . $this->expr->getStartLine() . " must be a scalar string");
// ConstExprEvaluator has already reduced concatenations and
// other constant string expressions to their PHP value. Emit
// that value as a C string literal instead of rejecting every
// non-literal string expression.
return '"' . getTranslator()->escapeString((string) $this->value) . '"';
}
$expr = preg_replace("/(^'|'$)/", '"', getTranslator()->escapeString($expr));
} elseif ($this->type->isInt() or $this->type->isFloat()) {
@ -3306,6 +3314,11 @@ class PropertyInfo extends VariableLike
$defaultValue = EvaluatedValue::null();
} else {
$defaultValue = EvaluatedValue::createFromExpression($this->defaultValue, null, null, $allConstInfos);
if ($simpleType !== null && $simpleType->isFloat() && $defaultValue->type->isInt()) {
// PHP permits an integer default for a float property and
// stores it as a double in the class default table.
$defaultValue->type = $simpleType;
}
if ($defaultValue->isUnknownConstValue || ($defaultValue->originatingConsts && $defaultValue->getCExpr() === null)) {
echo "Skipping code generation for property $this->name, because it has an unknown constant default value\n";
return "";
@ -6365,16 +6378,16 @@ function initPhpParser() {
$isInitialized = true;
}
function formatConstValue(mixed $constValue)
function normalizeConstExprValue(mixed $constValue): mixed
{
if (is_string($constValue)) {
if (str_starts_with($constValue, '"') and str_ends_with($constValue, '"')) {
return $constValue;
}
return '"' . $constValue . '"';
} else {
return $constValue;
if (is_string($constValue)
&& strlen($constValue) >= 2
&& $constValue[0] === '"'
&& $constValue[strlen($constValue) - 1] === '"'
) {
return stripcslashes(substr($constValue, 1, -1));
}
return $constValue;
}
function getTranslator(): Translator

@ -0,0 +1,53 @@
--TEST--
Property defaults support constant expressions, inheritance, and traits
--FILE--
<?php
const GLOBAL_NUMBER = 4;
trait PropertyDefaultsTrait
{
public int $traitValue = 10 + 1;
public static string $traitStatic = 'trait' . '-static';
}
class PropertyDefaultsParent
{
private const LABEL_PREFIX = 'parent';
public int $sum = 1 + 2;
public float $ratio = 2;
protected string $label = self::LABEL_PREFIX . '-value';
public static int $counter = GLOBAL_NUMBER + 1;
public static float $staticRatio = 3;
public function label(): string
{
return $this->label;
}
}
class PropertyDefaultsChild extends PropertyDefaultsParent
{
use PropertyDefaultsTrait;
}
function main(): void
{
$value = new PropertyDefaultsChild();
var_dump($value->sum, $value->ratio, $value->label(), $value->traitValue);
var_dump(
PropertyDefaultsChild::$counter,
PropertyDefaultsChild::$staticRatio,
PropertyDefaultsChild::$traitStatic
);
}
?>
--EXPECT--
int(3)
float(2)
string(12) "parent-value"
int(11)
int(5)
float(3)
string(12) "trait-static"
Loading…
Cancel
Save