fix: 修复属性默认值初始化及类型检查问题 #24

Merged
韩天峰 merged 5 commits from fix-property-default-value into master 1 month ago
  1. 16
      phpunit/code/property-default-array-for-int.php
  2. 15
      phpunit/code/property-default-array-for-object.php
  3. 12
      phpunit/code/property-default-class-const-type.php
  4. 10
      phpunit/code/property-default-expression-type.php
  5. 10
      phpunit/code/property-default-false-for-true.php
  6. 11
      phpunit/code/property-default-null-for-int.php
  7. 11
      phpunit/code/property-default-string-for-int.php
  8. 10
      phpunit/code/property-default-true-for-false.php
  9. 23
      phpunit/code/property-default-valid.php
  10. 70
      phpunit/src/ClassTest.php
  11. 220
      src/Preprocessor.php
  12. 147
      src/Translator.php
  13. 41
      src/gen_stub.php
  14. 53
      tests/compiler/object_property/default-expressions-inheritance.phpt
  15. 52
      tests/compiler/object_property/default-mixed-array.phpt
  16. 55
      tests/compiler/object_property/default-values.phpt

@ -0,0 +1,16 @@
<?php
class PropertyDefaultArrayForInt
{
private int $a = [];
public function __construct()
{
var_dump($this->a);
}
}
function property_default_array_for_int(): void
{
$test = new PropertyDefaultArrayForInt();
}

@ -0,0 +1,15 @@
<?php
class PropertyDefaultArrayForObjectDep
{
}
class PropertyDefaultArrayForObject
{
public PropertyDefaultArrayForObjectDep $dep = [];
}
function property_default_array_for_object(): void
{
$test = new PropertyDefaultArrayForObject();
}

@ -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,11 @@
<?php
class PropertyDefaultNullForInt
{
public int $a = null;
}
function property_default_null_for_int(): void
{
$test = new PropertyDefaultNullForInt();
}

@ -0,0 +1,11 @@
<?php
class PropertyDefaultStringForInt
{
public int $a = 'hello';
}
function property_default_string_for_int(): void
{
$test = new PropertyDefaultStringForInt();
}

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

@ -0,0 +1,23 @@
<?php
class PropertyDefaultValid
{
public int $i = 123;
public float $f = 1.5;
public float $fromInt = 3;
public string $s = 'hello';
public bool $b = true;
public array $arr = [];
public ?int $ni = null;
public int|array $ia = [];
public mixed $m = [];
public $untyped = [];
public const NUM = 7;
public int $fromConst = self::NUM;
}
function property_default_valid(): void
{
$test = new PropertyDefaultValid();
var_dump($test->i);
}

@ -194,4 +194,74 @@ class ClassTest extends \BaseTest
// 作为函数/方法默认参数值应当能够在编译期正确解析。 // 作为函数/方法默认参数值应当能够在编译期正确解析。
$this->compile('class-const-default-value.php'); $this->compile('class-const-default-value.php');
} }
public function testPropertyDefaultArrayForIntTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use array as default value for property PropertyDefaultArrayForInt::$a of type int',
'property-default-array-for-int.php'
);
}
public function testPropertyDefaultStringForIntTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use string as default value for property PropertyDefaultStringForInt::$a of type int',
'property-default-string-for-int.php'
);
}
public function testPropertyDefaultNullForNonNullableIntFailsAtCompileTime()
{
$this->exec(
'Cannot use null as default value for property PropertyDefaultNullForInt::$a of type int',
'property-default-null-for-int.php'
);
}
public function testPropertyDefaultArrayForObjectTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use array as default value for property PropertyDefaultArrayForObject::$dep of type PropertyDefaultArrayForObjectDep',
'property-default-array-for-object.php'
);
}
public function testValidPropertyDefaultsCompile()
{
// 合法的默认值(含 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\PropertyHookLowering;
use TypePhp\Transform\Visitor; use TypePhp\Transform\Visitor;
use PhpParser\Modifiers; use PhpParser\Modifiers;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\IntersectionType; use PhpParser\Node\IntersectionType;
use PhpParser\Node\NullableType; use PhpParser\Node\NullableType;
@ -620,12 +621,20 @@ class Preprocessor extends CompilerBase
} }
$this->symbolDeclInFile[$fullClassNameLower] = $this->file; $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 = ''; $code = '';
foreach ($class->stmts as $v) { foreach ($class->stmts as $v) {
$type = $v->getType(); $type = $v->getType();
switch ($type) { switch ($type) {
case 'Stmt_ClassConst': case 'Stmt_ClassConst':
$this->parseClassConstDef($v);
break; break;
case 'Stmt_Property': case 'Stmt_Property':
$this->parseClassPropertyDef($v); $this->parseClassPropertyDef($v);
@ -891,10 +900,18 @@ class Preprocessor extends CompilerBase
$default = null; $default = null;
$arrayInitPlan = null; $arrayInitPlan = null;
if ($defaultNode !== null) { if ($defaultNode !== null) {
$this->checkPropertyDefaultType($name, $typeNode, $defaultNode, $errorNode);
if ($defaultNode instanceof Node\Expr\Array_) { if ($defaultNode instanceof Node\Expr\Array_) {
$type = Type::ARRAY;
$arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode); $arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode);
$default = $arrayInitPlan->expr; $default = $arrayInitPlan->expr;
// Only narrow the property type to `array` when the declared type
// cannot already hold an array. `mixed`/`iterable`/union/nullable
// types are represented as php::Var and can legally store an array,
// so forcing `array` here would wrongly reject non-array assignments
// (e.g. `mixed $value = []` followed by `$this->value = 123`).
if ($type !== Type::VAR) {
$type = Type::ARRAY;
}
} else { } else {
$default = $this->parseIdentifier($defaultNode); $default = $this->parseIdentifier($defaultNode);
} }
@ -918,6 +935,205 @@ class Preprocessor extends CompilerBase
return $propDef; return $propDef;
} }
/**
* Diagnose, during preprocessing, whether a property's default value is
* compatible with its declared type.
*
* TypePHP rejects obvious mismatches such as `int $a = []` at compile time
* instead of silently coercing the declared type or deferring to a runtime
* TypeError, matching the static-compilation principles in CLAUDE.md.
*/
protected function checkPropertyDefaultType(string $name, ?NodeAbstract $typeNode, NodeAbstract $defaultNode, NodeAbstract $errorNode): void
{
if ($typeNode === null) {
// Untyped property accepts any default value.
return;
}
$valueType = $this->detectDefaultValueType($defaultNode);
if ($valueType === null) {
// The value type is not statically decidable (e.g. user or class
// constant references); leave it to later stages.
return;
}
$allowed = $this->collectAllowedDefaultTypes($typeNode);
if ($allowed === null) {
// mixed / callable / otherwise unconstrained type declaration.
return;
}
if (in_array($valueType, $allowed, true)) {
return;
}
$className = $this->getFullClassName();
$typeStr = $this->propertyTypeDeclToString($typeNode);
$this->fatalError(
$errorNode,
"Cannot use {$valueType} as default value for property {$className}::\${$name} of type {$typeStr}"
);
}
/**
* Determine the PHP value type of a constant expression used as a default
* 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 $scopeClass = null, int $depth = 0): ?string
{
if ($depth > 16) {
return null;
}
$scopeClass ??= $this->getFullClassName();
switch ($node->getType()) {
case 'Scalar_Int':
return 'int';
case 'Scalar_Float':
return 'float';
case 'Scalar_String':
case 'Scalar_InterpolatedString':
case 'Expr_BinaryOp_Concat':
return 'string';
case 'Expr_Array':
return 'array';
case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus':
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
case 'Expr_ConstFetch':
return match (strtolower($node->name->toString())) {
'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:
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,
};
}
}
/**
* Collect the set of value types accepted as a default for a declared type
* node. Returns null when the type imposes no statically-checkable
* constraint (mixed / callable / unknown).
*
* @return array<int, string>|null
*/
protected function collectAllowedDefaultTypes(NodeAbstract $typeNode): ?array
{
if ($typeNode instanceof NullableType) {
$inner = $this->collectAllowedDefaultTypes($typeNode->type);
if ($inner === null) {
return null;
}
return array_values(array_unique(array_merge($inner, ['null'])));
}
if ($typeNode instanceof UnionType) {
$all = [];
foreach ($typeNode->types as $sub) {
$part = $this->collectAllowedDefaultTypes($sub);
if ($part === null) {
// A mixed-like member accepts any default value.
return null;
}
$all = array_merge($all, $part);
}
return array_values(array_unique($all));
}
if ($typeNode instanceof IntersectionType) {
// Intersection types are object-only; no scalar/array default valid.
return [];
}
return match (strtolower($this->parseIdentifier($typeNode))) {
'int' => ['int'],
'float', 'double' => ['float', 'int'], // int coerces to float
'string' => ['string'],
'bool' => ['true', 'false'],
'true' => ['true'],
'false' => ['false'],
'array' => ['array'],
'iterable' => ['array'],
'null' => ['null'],
'object' => [], // no literal object default exists
'self', 'parent', 'static' => [],
'mixed' => null,
'callable' => null, // string/array/closure — not checkable
default => [], // class type: only null via ?Type
};
}
protected function propertyTypeDeclToString(NodeAbstract $typeNode): string
{
if ($typeNode instanceof NullableType) {
return '?' . $this->propertyTypeDeclToString($typeNode->type);
}
if ($typeNode instanceof UnionType) {
$parts = [];
foreach ($typeNode->types as $t) {
$parts[] = $this->propertyTypeDeclToString($t);
}
return implode('|', $parts);
}
if ($typeNode instanceof IntersectionType) {
$parts = [];
foreach ($typeNode->types as $t) {
$parts[] = $this->propertyTypeDeclToString($t);
}
return implode('&', $parts);
}
return $this->parseIdentifier($typeNode);
}
protected function parseClassPropertyDef(Node\Stmt\Property $v): void protected function parseClassPropertyDef(Node\Stmt\Property $v): void
{ {
$oriCtx = $this->context; $oriCtx = $this->context;

@ -866,15 +866,36 @@ CODE;
$code .= '// static property ' . PHP_EOL; $code .= '// static property ' . PHP_EOL;
foreach ($this->symbols->classes() as $classDef) { foreach ($this->symbols->classes() as $classDef) {
// Traits are never instantiated on their own; their static properties
// live on the classes that use them (where the members are flattened).
// Initialising a default on the trait itself would write to the trait's
// static property table and, on PHP >= 8.3, trigger a
// "Accessing static trait property" deprecation when the value is read
// through `self::` from a consuming class. Skip traits here; the
// consuming classes still initialise their own (flattened) copies.
if ($classDef->trait) {
continue;
}
foreach ($classDef->properties as $property) { foreach ($classDef->properties as $property) {
if (!$property->isStatic() || !$property->arrayInitPlan || !$property->default) { if (!$property->isStatic() || $property->default === null) {
continue; continue;
} }
$statement = 'php::setStaticProperty(' if ($property->arrayInitPlan) {
. $this->genCharPtr($classDef->getNamespacedName(false), true) . ', ' $statement = 'php::setStaticProperty('
. $this->genCharPtr($property->name) . ', ' . $this->genCharPtr($classDef->getNamespacedName(false), true) . ', '
. $property->arrayInitPlan->expr . ');' . PHP_EOL; . $this->genCharPtr($property->name) . ', '
$code .= $this->wrapArrayInitPlan($property->arrayInitPlan, $statement); . $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(' . $default . '));' . PHP_EOL;
$code .= $statement;
}
} }
} }
@ -1609,19 +1630,63 @@ CODE;
if ($classDef && !$classDef->trait && !$classDef->enum) { if ($classDef && !$classDef->trait && !$classDef->enum) {
$className = $classDef->getNamespacedName(); $className = $classDef->getNamespacedName();
$handlers = "property_handlers_{$className}"; $handlers = "property_handlers_{$className}";
$buildCreateBody = function (bool $attachHandlers) use ($classDef, $className, $handlers): string { $initBlock = '';
foreach ($classDef->properties as $property) {
if ($property->isStatic() || $property->default === null) {
continue;
}
if ($property->arrayInitPlan) {
$init = "auto value = {$property->arrayInitPlan->expr};\n";
$init .= 'zend_update_property(obj->ce, obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n";
$init .= "php::throwErrorIfOccurred();\n";
$initBlock .= $this->wrapArrayInitPlan($property->arrayInitPlan, $init);
} else {
// Scalar / constant / null default value. Wrap it in a
// php::Var so it can be stored as a zval in the object's
// 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({$default});\n";
$init .= 'zend_update_property(obj->ce, obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n";
$init .= "php::throwErrorIfOccurred();\n";
$init .= "} while (0);\n";
$initBlock .= $init;
}
}
$buildCreateBody = function (bool $attachHandlers) use ($classDef, $className, $handlers, $ce, $initBlock): string {
$body = $classDef->ctorInit; $body = $classDef->ctorInit;
$body .= "auto obj = create_object_{$className}(class_type);\n";
if ($attachHandlers) { if ($attachHandlers) {
// PHP < 8.4: the custom handlers are attached to the object
// AFTER the standard create_object, so object_properties_init
// runs with the standard handlers (no asymmetric check). Our
// explicit default inits run with the custom handlers
// attached, so we set EG(fake_scope) to the object's own class
// to satisfy asymmetric visibility for the class's own
// properties.
$body .= "auto obj = create_object_{$className}(class_type);\n";
$body .= "typephp_attach_property_handlers(obj, &{$handlers});\n"; $body .= "typephp_attach_property_handlers(obj, &{$handlers});\n";
} $body .= "zend_class_entry *__typephp_saved_fake_scope = EG(fake_scope);\n";
foreach ($classDef->properties as $property) { $body .= "EG(fake_scope) = obj->ce;\n";
if (!$property->isStatic() && $property->arrayInitPlan && $property->default) { $body .= $initBlock;
$init = "auto value = {$property->arrayInitPlan->expr};\n"; $body .= "EG(fake_scope) = __typephp_saved_fake_scope;\n";
$init .= 'zend_update_property(obj->ce, obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n"; } else {
$init .= "php::throwErrorIfOccurred();\n"; // PHP >= 8.4: the custom handlers live in
$body .= $this->wrapArrayInitPlan($property->arrayInitPlan, $init); // default_object_handlers, so the object already carries the
} // asymmetric write_property hook at creation time and
// object_properties_init would reject private(set)/protected(set)
// default values (including inherited ones). Create the object
// with the standard handlers, run the default initialization
// (no visibility check), then attach the custom handlers.
$body .= "auto obj = zend_objects_new(class_type);\n";
$body .= "obj->handlers = const_cast<zend_object_handlers *>(zend_get_std_object_handlers());\n";
$body .= "object_properties_init(obj, class_type);\n";
$body .= $initBlock;
$body .= "obj->handlers = &{$handlers};\n";
} }
$body .= $classDef->ctorClean; $body .= $classDef->ctorClean;
return $body . "return obj;\n"; return $body . "return obj;\n";
@ -1633,7 +1698,7 @@ CODE;
$code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n"; $code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n";
$code .= $buildCreateBody(true); $code .= $buildCreateBody(true);
$code .= "};\n"; $code .= "};\n";
if ($classDef->requireCtor) { if ($classDef->requireCtor || $this->classHasAsymmetricOrHookedProperty($classDef)) {
$code .= "#else\n"; $code .= "#else\n";
$code .= "create_object_{$className} = php_get_create_object_fn({$ce});\n"; $code .= "create_object_{$className} = php_get_create_object_fn({$ce});\n";
$code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n"; $code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n";
@ -1646,6 +1711,46 @@ CODE;
return $code; return $code;
} }
/**
* Whether the given class (or any of its ancestors) declares an asymmetric
* visibility property (private(set)/protected(set)) or a hooked property
* (getter/setter). Such classes install a custom write_property handler, and
* on PHP >= 8.4 that handler lives in the class's default object handlers, so
* the engine's object_properties_init would reject inherited default values
* unless we generate our own create_object that initializes with the standard
* handlers first.
*/
private function classHasAsymmetricOrHookedProperty(ClassDef $classDef): bool
{
$current = $classDef;
$seen = [];
while ($current !== null) {
$key = strtolower(ltrim($current->getNamespacedName(), '\\'));
if (isset($seen[$key])) {
break;
}
$seen[$key] = true;
foreach ($current->properties as $property) {
if ($property->isPrivateSet()
|| $property->isProtectedSet()
|| $property->getter !== null
|| $property->setter !== null
) {
return true;
}
}
if (!$current->extends) {
break;
}
$parent = $this->getClassDef($current->extends);
if ($parent === null) {
break;
}
$current = $parent;
}
return false;
}
protected function getRegisterClassFunction(string $name): string protected function getRegisterClassFunction(string $name): string
{ {
return self::PREFIX . 'register_class_' . $name; return self::PREFIX . 'register_class_' . $name;
@ -2884,13 +2989,13 @@ CODE;
// 接口没有方法实体 // 接口没有方法实体
if ($classDef instanceof ClassDef) { if ($classDef instanceof ClassDef) {
$arrayPropCount = 0; $defaultPropCount = 0;
foreach ($classDef->properties as $property) { foreach ($classDef->properties as $property) {
if ($property->type === Type::ARRAY && $property->arrayInitPlan && $property->default && !$property->isStatic()) { if (!$property->isStatic() && $property->default !== null) {
$arrayPropCount++; $defaultPropCount++;
} }
} }
if ($arrayPropCount > 0) { if ($defaultPropCount > 0) {
$classDef->requireCtor = true; $classDef->requireCtor = true;
} }
$methods = $classDef->methods; $methods = $classDef->methods;

@ -2371,12 +2371,16 @@ class EvaluatedValue
if ($class === 'self') { if ($class === 'self') {
$constName = ClassInfo::$currentClass . "::" . $constName; $constName = ClassInfo::$currentClass . "::" . $constName;
if (isset($allConstInfos[$constName])) { if (isset($allConstInfos[$constName])) {
return formatConstValue($allConstInfos[$constName]->getValue($allConstInfos)->value); return $allConstInfos[$constName]->getValue($allConstInfos)->value;
} else { } else {
return formatConstValue(getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $constName)); return normalizeConstExprValue(
getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $constName)
);
} }
} else { } else {
return formatConstValue(getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass)); return normalizeConstExprValue(
getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass)
);
} }
} else { } else {
$constName = $expr->name->__toString(); $constName = $expr->name->__toString();
@ -2414,7 +2418,7 @@ class EvaluatedValue
if (isset($definedConstants[$constName])) { if (isset($definedConstants[$constName])) {
$constValue = $definedConstants[$constName]; $constValue = $definedConstants[$constName];
if (is_scalar($constValue)) { if (is_scalar($constValue)) {
return formatConstValue($constValue); return $constValue;
} }
} }
@ -2508,11 +2512,15 @@ class EvaluatedValue
// fully qualified class name (already stored in $this->value). // fully qualified class name (already stored in $this->value).
return '"' . addcslashes($this->value, '\\') . '"'; return '"' . addcslashes($this->value, '\\') . '"';
} }
return $this->value; return '"' . getTranslator()->escapeString((string) $this->value) . '"';
} elseif ($this->expr instanceof Expr\ConstFetch) { } elseif ($this->expr instanceof Expr\ConstFetch) {
return getTranslator()->getConstValue($this->expr->name->toString()); return getTranslator()->getConstValue($this->expr->name->toString());
} elseif (!($this->expr instanceof String_)) { } 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)); $expr = preg_replace("/(^'|'$)/", '"', getTranslator()->escapeString($expr));
} elseif ($this->type->isInt() or $this->type->isFloat()) { } elseif ($this->type->isInt() or $this->type->isFloat()) {
@ -3306,6 +3314,11 @@ class PropertyInfo extends VariableLike
$defaultValue = EvaluatedValue::null(); $defaultValue = EvaluatedValue::null();
} else { } else {
$defaultValue = EvaluatedValue::createFromExpression($this->defaultValue, null, null, $allConstInfos); $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)) { 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"; echo "Skipping code generation for property $this->name, because it has an unknown constant default value\n";
return ""; return "";
@ -6365,16 +6378,16 @@ function initPhpParser() {
$isInitialized = true; $isInitialized = true;
} }
function formatConstValue(mixed $constValue) function normalizeConstExprValue(mixed $constValue): mixed
{ {
if (is_string($constValue)) { if (is_string($constValue)
if (str_starts_with($constValue, '"') and str_ends_with($constValue, '"')) { && strlen($constValue) >= 2
return $constValue; && $constValue[0] === '"'
} && $constValue[strlen($constValue) - 1] === '"'
return '"' . $constValue . '"'; ) {
} else { return stripcslashes(substr($constValue, 1, -1));
return $constValue;
} }
return $constValue;
} }
function getTranslator(): Translator 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"

@ -0,0 +1,52 @@
--TEST--
property default value is array with mixed declared type
--FILE--
<?php
declare(strict_types=1);
class Test
{
private mixed $value = [];
public function __construct(mixed $value)
{
$this->value = $value;
}
public function getValue(): mixed
{
return $this->value;
}
}
function main()
{
$test = new Test(123);
var_dump($test->getValue());
$test = new Test('test');
var_dump($test->getValue());
$test = new Test([1, 2, 3]);
var_dump($test->getValue());
$test = new Test(new stdClass);
$v = $test->getValue();
var_dump($v instanceof stdClass);
var_dump(get_class($v));
}
?>
--EXPECT--
int(123)
string(4) "test"
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
bool(true)
string(8) "stdClass"

@ -0,0 +1,55 @@
--TEST--
various property default values (array, int, float, string, bool, null, const)
--FILE--
<?php
class Test
{
public $untypedArray = [];
public mixed $mixedArray = [];
public array $typedArray = [];
public $untypedInt = 123;
public int $typedInt = 123;
public float $typedFloat = 1.5;
public string $typedString = 'hello';
public bool $typedBool = true;
public $untypedNull = null;
public $untypedConst = PHP_INT_MAX;
public function show(): void
{
var_dump(
$this->untypedArray,
$this->mixedArray,
$this->typedArray,
$this->untypedInt,
$this->typedInt,
$this->typedFloat,
$this->typedString,
$this->typedBool,
$this->untypedNull,
$this->untypedConst
);
}
}
function main()
{
$t = new Test();
$t->show();
}
?>
--EXPECT--
array(0) {
}
array(0) {
}
array(0) {
}
int(123)
int(123)
float(1.5)
string(5) "hello"
bool(true)
NULL
int(9223372036854775807)
Loading…
Cancel
Save