diff --git a/composer.json b/composer.json index 429acfec..d36e9284 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^11.0 | ^10.0", - "xp-framework/ast": "^8.0", + "xp-framework/ast": "^8.1", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d7d7728f..2544977a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -383,7 +383,7 @@ protected function emitEnum($result, $enum) { protected function emitClass($result, $class) { array_unshift($result->type, $class); array_unshift($result->meta, []); - $result->locals= [[], [], []]; + $result->locals ?: $result->locals= [[], [], []]; $class->comment && $this->emitOne($result, $class->comment); $class->annotations && $this->emitOne($result, $class->annotations); @@ -399,21 +399,24 @@ protected function emitClass($result, $class) { if ($result->locals[2]) { $result->out->write('private $__virtual= ['); foreach ($result->locals[2] as $name => $access) { - $result->out->write("'{$name}' => null,"); + $name && $result->out->write("'{$name}' => null,"); } $result->out->write('];'); $result->out->write('public function __get($name) { switch ($name) {'); foreach ($result->locals[2] as $name => $access) { - $result->out->write('case "'.$name.'":'); + $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[0]); $result->out->write('break;'); } - $result->out->write('default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING); }}'); + isset($result->locals[2][null]) || $result->out->write( + 'default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING);' + ); + $result->out->write('}}'); $result->out->write('public function __set($name, $value) { switch ($name) {'); foreach ($result->locals[2] as $name => $access) { - $result->out->write('case "'.$name.'":'); + $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[1]); $result->out->write('break;'); } @@ -433,6 +436,7 @@ protected function emitClass($result, $class) { $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name.'::__init();'); array_shift($result->type); + $result->locals= []; } protected function emitMeta($result, $name, $annotations, $comment) { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 6962bb4b..5d7be79e 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -19,6 +19,7 @@ class PHP70 extends PHP { OmitArgumentNames, OmitConstModifiers, OmitPropertyTypes, + ReadonlyClasses, ReadonlyProperties, RewriteClassOnObjects, RewriteEnums, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 4aabf251..8ac6ba0f 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -19,6 +19,7 @@ class PHP71 extends PHP { OmitArgumentNames, OmitPropertyTypes, ReadonlyProperties, + ReadonlyClasses, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 2c9aa978..23a0bdc6 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -19,6 +19,7 @@ class PHP72 extends PHP { OmitArgumentNames, OmitPropertyTypes, ReadonlyProperties, + ReadonlyClasses, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index bf7c1e87..25e9a286 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -17,6 +17,7 @@ class PHP74 extends PHP { NonCapturingCatchVariables, NullsafeAsTernaries, OmitArgumentNames, + ReadonlyClasses, ReadonlyProperties, RewriteBlockLambdaExpressions, RewriteClassOnObjects, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 779c224c..af4026a6 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,8 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums; + use ReadonlyClasses, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 1317fc28..4cc19ab2 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, ReadonlyClasses; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index df654511..78dc8370 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_82 */ class PHP82 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, ReadonlyClasses; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php new file mode 100755 index 00000000..ab29f97f --- /dev/null +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -0,0 +1,36 @@ +modifiers))) { + unset($class->modifiers[$p]); + + // Inherit + foreach ($class->body as $member) { + if ($member->is('property')) { + $member->modifiers[]= 'readonly'; + } else if ($member->is('method')) { + foreach ($member->signature->parameters as $param) { + $param->promote && $param->promote.= ' readonly'; + } + } + } + + // Prevent dynamic members + $throw= new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);'); + $result->locals= [[], [], [null => [$throw, $throw]]]; + } + + return parent::emitClass($result, $class); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php similarity index 70% rename from src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php rename to src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 2f65771c..f13ae5a5 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -4,11 +4,12 @@ use unittest\{Assert, Expect, Test}; /** - * Readonly properties + * Readonly classes and properties * * @see https://wiki.php.net/rfc/readonly_properties_v2 + * @see https://wiki.php.net/rfc/readonly_classes */ -class ReadonlyPropertiesTest extends EmittingTest { +class ReadonlyTest extends EmittingTest { /** @return iterable */ private function modifiers() { @@ -20,7 +21,19 @@ private function modifiers() { } #[Test] - public function declaration() { + public function class_declaration() { + $t= $this->type('readonly class { + public int $fixture; + }'); + + Assert::equals( + sprintf('public readonly int %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); + } + + #[Test] + public function property_declaration() { $t= $this->type('class { public readonly int $fixture; }'); @@ -32,11 +45,28 @@ public function declaration() { } #[Test] - public function with_constructor_argument_promotion() { + public function class_with_constructor_argument_promotion() { + $t= $this->type('readonly class { + public function __construct(public string $fixture) { } + }'); + + Assert::equals( + sprintf('public readonly string %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function property_defined_with_constructor_argument_promotion() { $t= $this->type('class { public function __construct(public readonly string $fixture) { } }'); + Assert::equals( + sprintf('public readonly string %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); Assert::equals('Test', $t->newInstance('Test')->fixture); } @@ -143,4 +173,23 @@ public function cannot_have_an_initial_value() { public readonly string $fixture= "Test"; }'); } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot create dynamic property .+fixture/')] + public function cannot_read_dynamic_members_from_readonly_classes() { + $t= $this->type('readonly class { }'); + $t->newInstance()->fixture; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot create dynamic property .+fixture/')] + public function cannot_write_dynamic_members_from_readonly_classes() { + $t= $this->type('readonly class { }'); + $t->newInstance()->fixture= true; + } + + #[Test, Ignore('Until proper error handling facilities exist')] + public function readonly_classes_cannot_have_static_members() { + $this->type('readonly class { + public static $test; + }'); + } } \ No newline at end of file