From 401ef99b005e84a89507b6a884cbb66f40118345 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 13:55:18 +0200 Subject: [PATCH 01/23] Add emitting support for asymmetric visibility --- .../ast/emit/AsymmetricVisibility.class.php | 37 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP81.class.php | 4 +- src/main/php/lang/ast/emit/PHP82.class.php | 4 +- src/main/php/lang/ast/emit/PHP83.class.php | 2 +- .../lang/ast/emit/RewriteProperties.class.php | 7 +++- .../emit/AsymmetricVisibilityTest.class.php | 28 ++++++++++++++ 6 files changed, 75 insertions(+), 7 deletions(-) create mode 100755 src/main/php/lang/ast/emit/AsymmetricVisibility.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php new file mode 100755 index 00000000..f83f856b --- /dev/null +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -0,0 +1,37 @@ +name}'"); + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); + + if (in_array('private(set)', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' + ); + } + + $scope= $result->codegen->scope[0]; + $scope->virtual[$property->name]= [ + new ReturnStatement($virtual), + new Block([new Code($check), new Assignment($virtual, '=', new Variable('value'))]), + ]; + if (isset($property->expression)) { + $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 70d51ba5..de5db7dc 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -23,9 +23,9 @@ class PHP81 extends PHP { RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, + RewriteProperties, ReadonlyClasses, - OmitConstantTypes, - PropertyHooks + OmitConstantTypes ; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 0b568d29..8d0c1b96 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -23,9 +23,9 @@ class PHP82 extends PHP { RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, + RewriteProperties, ReadonlyClasses, - OmitConstantTypes, - PropertyHooks + OmitConstantTypes ; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 9285e65d..7ae725f7 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -18,7 +18,7 @@ * @see https://wiki.php.net/rfc#php_83 */ class PHP83 extends PHP { - use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks; + use RewriteBlockLambdaExpressions, RewriteProperties, ReadonlyClasses; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index 29a0b4d6..e4cb7bb9 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -1,15 +1,18 @@ hooks) { return $this->emitPropertyHooks($result, $property); - } else if (in_array('readonly', $property->modifiers)) { + } else if (in_array('private(set)', $property->modifiers)) { + return $this->emitAsymmetricVisibility($result, $property); + } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); } parent::emitProperty($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php new file mode 100755 index 00000000..ca97d0d1 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -0,0 +1,28 @@ +declare('class %T { + public private(set) $fixture= "Test"; + }'); + Assert::equals('Test', $t->newInstance()->fixture); + } + + #[Test, Expect(class: Error::class, message: '/Cannot access private property T.+::fixture/')] + public function writing() { + $t= $this->declare('class %T { + public private(set) $fixture= "Test"; + }'); + $t->newInstance()->fixture= 'Changed'; + } +} \ No newline at end of file From a6be1f6cb0f16647b910a4585464fe21541a1767 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 13:59:52 +0200 Subject: [PATCH 02/23] Require feature branch See xp-framework/ast#54 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ed40d3f1..1d63b8cc 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", "xp-framework/reflection": "^3.0 | ^2.13", - "xp-framework/ast": "^11.1", + "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", "php" : ">=7.4.0" }, "require-dev" : { From 7172e6ee52605965ad941c2ff42412587dd17c92 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:02:58 +0200 Subject: [PATCH 03/23] Add support for protected(set) --- .../php/lang/ast/emit/AsymmetricVisibility.class.php | 6 ++++++ src/main/php/lang/ast/emit/RewriteProperties.class.php | 2 +- .../unittest/emit/AsymmetricVisibilityTest.class.php | 10 +++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index f83f856b..d7e85488 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -23,6 +23,12 @@ protected function emitProperty($result, $property) { 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' ); + } else if (in_array('protected(set)', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' + ); } $scope= $result->codegen->scope[0]; diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index e4cb7bb9..6715fd20 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -10,7 +10,7 @@ trait RewriteProperties { protected function emitProperty($result, $property) { if ($property->hooks) { return $this->emitPropertyHooks($result, $property); - } else if (in_array('private(set)', $property->modifiers)) { + } else if (in_array('private(set)', $property->modifiers) || in_array('protected(set)', $property->modifiers)) { return $this->emitAsymmetricVisibility($result, $property); } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index ca97d0d1..e02bc9ca 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -19,10 +19,18 @@ public function reading() { } #[Test, Expect(class: Error::class, message: '/Cannot access private property T.+::fixture/')] - public function writing() { + public function writing_private() { $t= $this->declare('class %T { public private(set) $fixture= "Test"; }'); $t->newInstance()->fixture= 'Changed'; } + + #[Test, Expect(class: Error::class, message: '/Cannot access protected property T.+::fixture/')] + public function writing_protected() { + $t= $this->declare('class %T { + public protected(set) $fixture= "Test"; + }'); + $t->newInstance()->fixture= 'Changed'; + } } \ No newline at end of file From 6760f345f911af0f95a619d9c0d8fa50b68d36a9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:41:20 +0200 Subject: [PATCH 04/23] Make errors consistent with PHP implementation See https://github.com/php/php-src/pull/15063/files#diff-5a3c9d6fc60178fb68e7c0fde07798c26bb71277f57f265043c1faf329b6ba25R43 --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 4 ++-- .../lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index d7e85488..48d6096a 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -21,13 +21,13 @@ protected function emitProperty($result, $property) { $check= ( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' + 'throw new \\Error("Cannot modify private(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' ); } else if (in_array('protected(set)', $property->modifiers)) { $check= ( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' + 'throw new \\Error("Cannot modify protected(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' ); } diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index e02bc9ca..adf42921 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -18,7 +18,7 @@ public function reading() { Assert::equals('Test', $t->newInstance()->fixture); } - #[Test, Expect(class: Error::class, message: '/Cannot access private property T.+::fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { public private(set) $fixture= "Test"; @@ -26,7 +26,7 @@ public function writing_private() { $t->newInstance()->fixture= 'Changed'; } - #[Test, Expect(class: Error::class, message: '/Cannot access protected property T.+::fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot modify protected\(set\) property T.+::\$fixture/')] public function writing_protected() { $t= $this->declare('class %T { public protected(set) $fixture= "Test"; From 865d2e982b733da4a8aa1fc11e602e5be280bce5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:44:42 +0200 Subject: [PATCH 05/23] Test writing from within private context --- .../emit/AsymmetricVisibilityTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index adf42921..3d4f8bec 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -18,6 +18,19 @@ public function reading() { Assert::equals('Test', $t->newInstance()->fixture); } + #[Test] + public function writing() { + $t= $this->declare('class %T { + public private(set) $fixture= "Test"; + + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); + } + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { From f281452e8f4f3d4f4363ea557c94b169736ffaae Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:48:02 +0200 Subject: [PATCH 06/23] Test promoted constructor parameters --- .../ast/unittest/emit/AsymmetricVisibilityTest.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 3d4f8bec..3c5781c7 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -46,4 +46,12 @@ public function writing_protected() { }'); $t->newInstance()->fixture= 'Changed'; } + + #[Test] + public function promoted_constructor_parameter() { + $t= $this->declare('class %T { + public function __construct(public private(set) $fixture) { } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } } \ No newline at end of file From 72cde2318956296fc49aaf1474ff0d9f1ee50386 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 15:05:46 +0200 Subject: [PATCH 07/23] Allow explicit `public(set)` --- .../lang/ast/emit/AsymmetricVisibility.class.php | 12 +++++++----- .../php/lang/ast/emit/RewriteProperties.class.php | 2 +- .../emit/AsymmetricVisibilityTest.class.php | 13 ++++++++++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 48d6096a..66dbfaf2 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -18,23 +18,25 @@ protected function emitProperty($result, $property) { $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); if (in_array('private(set)', $property->modifiers)) { - $check= ( + $check= [new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot modify private(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - ); + )]; } else if (in_array('protected(set)', $property->modifiers)) { - $check= ( + $check= [new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot modify protected(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - ); + )]; + } else { + $check= []; } $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), - new Block([new Code($check), new Assignment($virtual, '=', new Variable('value'))]), + new Block([...$check, new Assignment($virtual, '=', new Variable('value'))]), ]; if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index 6715fd20..094a8298 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -10,7 +10,7 @@ trait RewriteProperties { protected function emitProperty($result, $property) { if ($property->hooks) { return $this->emitPropertyHooks($result, $property); - } else if (in_array('private(set)', $property->modifiers) || in_array('protected(set)', $property->modifiers)) { + } else if (array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)'])) { return $this->emitAsymmetricVisibility($result, $property); } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 3c5781c7..acfe9b73 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -19,7 +19,7 @@ public function reading() { } #[Test] - public function writing() { + public function writing_from_self_scope() { $t= $this->declare('class %T { public private(set) $fixture= "Test"; @@ -31,6 +31,17 @@ public function rename($name) { Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); } + #[Test] + public function writing_explicitely_public_set() { + $t= $this->declare('class %T { + public public(set) $fixture= "Test"; + }'); + + $instance= $t->newInstance(); + $instance->fixture= 'Changed'; + Assert::equals('Changed', $instance->fixture); + } + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { From ee835aaca8a532541503a3eacdf408b4d7e8fb24 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 15:07:47 +0200 Subject: [PATCH 08/23] QA: Code locality --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 66dbfaf2..346359e4 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -14,9 +14,6 @@ trait AsymmetricVisibility { protected function emitProperty($result, $property) { - $literal= new Literal("'{$property->name}'"); - $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); - if (in_array('private(set)', $property->modifiers)) { $check= [new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. @@ -33,6 +30,11 @@ protected function emitProperty($result, $property) { $check= []; } + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( + new Literal('__virtual'), + new Literal("'{$property->name}'")) + ); + $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), From b1f428cdcbb95e2cbc5936301f7de1fba397e310 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 15:27:32 +0200 Subject: [PATCH 09/23] Extract visibility checks into trait --- .../ast/emit/AsymmetricVisibility.class.php | 14 +---- .../php/lang/ast/emit/PropertyHooks.class.php | 16 +----- .../ast/emit/ReadonlyProperties.class.php | 55 +++++++++++-------- .../lang/ast/emit/VisibilityChecks.class.php | 22 ++++++++ 4 files changed, 61 insertions(+), 46 deletions(-) create mode 100755 src/main/php/lang/ast/emit/VisibilityChecks.class.php diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 346359e4..514516f1 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -1,6 +1,5 @@ modifiers)) { - $check= [new Code( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot modify private(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - )]; + $check= [$this->private($property->name, 'modify private(set)')]; } else if (in_array('protected(set)', $property->modifiers)) { - $check= [new Code( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot modify protected(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - )]; + $check= [$this->protected($property->name, 'modify protected(set)')]; } else { $check= []; } diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 621edbfb..1859c521 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -1,7 +1,6 @@ private('$name', 'access private'), ...$nodes]); } else if ($modifiers & MODIFIER_PROTECTED) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' - ); + return new Block([$this->protected('$name', 'access protected'), ...$nodes]); } else if (1 === sizeof($nodes)) { return $nodes[0]; } else { return new Block($nodes); } - - return new Block([new Code($check), ...$nodes]); } protected function emitEmulatedHooks($result, $property) { diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 3356b426..69b75de0 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -1,5 +1,15 @@ modifiers)) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");' - ); - } else if (in_array('protected', $property->modifiers)) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' - ); + if ($modifiers & MODIFIER_PRIVATE) { + $check= $this->private($property->name, 'access private'); + } else if ($modifiers & MODIFIER_PROTECTED) { + $check= $this->protected($property->name, 'access protected'); } else { - $check= ''; + $check= null; } + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( + new Literal('__virtual'), + new Literal("'{$property->name}'")) + ); + // Create virtual property implementing the readonly semantics $scope->virtual[$property->name]= [ - new Code(sprintf($check.'return $this->__virtual["%1$s"][0];', $property->name)), - new Code(sprintf( - ($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'). - 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. - '$this->__virtual["%1$s"]= [$value];', - $property->name - )), + $check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual), + new Block([ + $check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'), + new Code(sprintf( + 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. + '$this->__virtual["%1$s"]= [$value];', + $property->name + )), + new Assignment($virtual, '=', new Variable('value')) + ]), ]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/VisibilityChecks.class.php b/src/main/php/lang/ast/emit/VisibilityChecks.class.php new file mode 100755 index 00000000..3b702b54 --- /dev/null +++ b/src/main/php/lang/ast/emit/VisibilityChecks.class.php @@ -0,0 +1,22 @@ + Date: Sat, 24 Aug 2024 16:01:21 +0200 Subject: [PATCH 10/23] QA: API docs --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 514516f1..f4f07d23 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -10,6 +10,12 @@ Variable }; +/** + * Asymmetric Visibility + * + * @see https://wiki.php.net/rfc/asymmetric-visibility-v2 + * @test lang.ast.unittest.emit.AsymmetricVisibilityTest + */ trait AsymmetricVisibility { use VisibilityChecks; From 59ed2c1f48f53d46d2ad0ec9078185732e7c5752 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 16:12:20 +0200 Subject: [PATCH 11/23] Verify setting protected(set) from inherited scope --- .../emit/AsymmetricVisibilityTest.class.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index acfe9b73..5e59d59d 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -28,6 +28,22 @@ public function rename($name) { return $this; } }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); + Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); + } + + #[Test] + public function writing_from_inherited_scope() { + $parent= $this->declare('class %T { public protected(set) $fixture= "Test"; }'); + $t= $this->declare('class %T extends '.$parent->literal().' { + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); } From f553374ba350620f5b2eddfa292961d65ec4fb1b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 16:31:07 +0200 Subject: [PATCH 12/23] Support readonly --- .../lang/ast/emit/AsymmetricVisibility.class.php | 14 +++++++++----- .../php/lang/ast/emit/ReadonlyProperties.class.php | 2 +- .../php/lang/ast/emit/VisibilityChecks.class.php | 4 ++++ .../emit/AsymmetricVisibilityTest.class.php | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index f4f07d23..3fced9e3 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -20,12 +20,16 @@ trait AsymmetricVisibility { use VisibilityChecks; protected function emitProperty($result, $property) { + $checks= []; if (in_array('private(set)', $property->modifiers)) { - $check= [$this->private($property->name, 'modify private(set)')]; + $checks[]= $this->private($property->name, 'modify private(set)'); } else if (in_array('protected(set)', $property->modifiers)) { - $check= [$this->protected($property->name, 'modify protected(set)')]; - } else { - $check= []; + $checks[]= $this->protected($property->name, 'modify protected(set)'); + } + + // The readonly flag is really two flags in one: write-once and restricted(set) + if (in_array('readonly', $property->modifiers)) { + $checks[]= $this->initonce($property->name); } $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( @@ -36,7 +40,7 @@ protected function emitProperty($result, $property) { $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), - new Block([...$check, new Assignment($virtual, '=', new Variable('value'))]), + new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]), ]; if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 69b75de0..82530fec 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -66,8 +66,8 @@ protected function emitProperty($result, $property) { $check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual), new Block([ $check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'), + $this->initonce($property->name), new Code(sprintf( - 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. '$this->__virtual["%1$s"]= [$value];', diff --git a/src/main/php/lang/ast/emit/VisibilityChecks.class.php b/src/main/php/lang/ast/emit/VisibilityChecks.class.php index 3b702b54..45672731 100755 --- a/src/main/php/lang/ast/emit/VisibilityChecks.class.php +++ b/src/main/php/lang/ast/emit/VisibilityChecks.class.php @@ -4,6 +4,10 @@ trait VisibilityChecks { + private function initonce($name) { + return new Code('if (isset($this->__virtual["'.$name.'"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::\$'.$name.'");'); + } + private function private($name, $access) { return new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 5e59d59d..75dbe770 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -81,4 +81,18 @@ public function __construct(public private(set) $fixture) { } }'); Assert::equals('Test', $t->newInstance('Test')->fixture); } + + #[Test, Expect(class: Error::class, message: '/Cannot modify readonly property .+fixture/')] + public function readonly() { + $t= $this->declare('class %T { + + // public-read, protected-write, write-once property + public protected(set) readonly string $fixture= "Test"; + + public function rename() { + $this->fixture= "Changed"; // Will always error + } + }'); + $t->newInstance()->rename(); + } } \ No newline at end of file From 7fba42769f415029be686e5d718d5ca0d9e04459 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 09:15:26 +0200 Subject: [PATCH 13/23] Add reflection Requires https://github.com/xp-framework/reflection/pull/43 --- composer.json | 2 +- .../ast/emit/AsymmetricVisibility.class.php | 33 ++++++++++++++++--- .../emit/AsymmetricVisibilityTest.class.php | 14 +++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 1d63b8cc..5dcb4c8d 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", - "xp-framework/reflection": "^3.0 | ^2.13", + "xp-framework/reflection": "dev-feature/asymmetric-visibility as 3.2.0", "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", "php" : ">=7.4.0" }, diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 3fced9e3..60883f82 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -20,10 +20,37 @@ trait AsymmetricVisibility { use VisibilityChecks; protected function emitProperty($result, $property) { + static $lookup= [ + 'public' => MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => MODIFIER_READONLY, + 'private(set)' => 0x0400, + 'protected(set)' => 0x0800, + 'public(set)' => 0x1000, + ]; + + // Emit XP meta information for the reflection API + $scope= $result->codegen->scope[0]; + $modifiers= 0; + foreach ($property->modifiers as $name) { + $modifiers|= $lookup[$name]; + } + $scope->meta[self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [$modifiers] + ]; + $checks= []; - if (in_array('private(set)', $property->modifiers)) { + if ($modifiers & 0x0400) { $checks[]= $this->private($property->name, 'modify private(set)'); - } else if (in_array('protected(set)', $property->modifiers)) { + } else if ($modifiers & 0x0800) { $checks[]= $this->protected($property->name, 'modify protected(set)'); } @@ -36,8 +63,6 @@ protected function emitProperty($result, $property) { new Literal('__virtual'), new Literal("'{$property->name}'")) ); - - $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]), diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 75dbe770..d822373c 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -1,7 +1,7 @@ newInstance()->rename(); } + + #[Test, Values(['private', 'protected', 'public'])] + public function reflection($modifier) { + $t= $this->declare('class %T { + public '.$modifier.'(set) string $fixture= "Test"; + }'); + + Assert::equals( + 'public '.$modifier.'(set) string $fixture', + $t->property('fixture')->toString() + ); + } } \ No newline at end of file From c1fb7dd2a01c759d7ec672bb9cad3b0ed66d76d6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 11:08:24 +0200 Subject: [PATCH 14/23] Readonly implies `protected(set)` --- .../php/lang/ast/unittest/emit/ReadonlyTest.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 9c8d1db5..a998a7dd 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -27,7 +27,7 @@ public function class_declaration() { }'); Assert::equals( - 'public readonly int $fixture', + 'public readonly protected(set) int $fixture', $t->property('fixture')->toString() ); } @@ -39,7 +39,7 @@ public function property_declaration() { }'); Assert::equals( - 'public readonly int $fixture', + 'public readonly protected(set) int $fixture', $t->property('fixture')->toString() ); } @@ -51,7 +51,7 @@ public function __construct(public string $fixture) { } }'); Assert::equals( - 'public readonly string $fixture', + 'public readonly protected(set) string $fixture', $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); @@ -64,7 +64,7 @@ public function __construct(public readonly string $fixture) { } }'); Assert::equals( - 'public readonly string $fixture', + 'public readonly protected(set) string $fixture', $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); From 241655713af7bcae0d2a0c6048a1d593a8697367 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 11:10:07 +0200 Subject: [PATCH 15/23] Adjust constants after integration-testing with PHP compiled from PR --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 60883f82..7692bf84 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -28,9 +28,9 @@ protected function emitProperty($result, $property) { 'final' => MODIFIER_FINAL, 'abstract' => MODIFIER_ABSTRACT, 'readonly' => MODIFIER_READONLY, - 'private(set)' => 0x0400, + 'public(set)' => 0x0400, 'protected(set)' => 0x0800, - 'public(set)' => 0x1000, + 'private(set)' => 0x1000, ]; // Emit XP meta information for the reflection API @@ -48,7 +48,7 @@ protected function emitProperty($result, $property) { ]; $checks= []; - if ($modifiers & 0x0400) { + if ($modifiers & 0x1000) { $checks[]= $this->private($property->name, 'modify private(set)'); } else if ($modifiers & 0x0800) { $checks[]= $this->protected($property->name, 'modify protected(set)'); From 7c9cee8289442acfb9dc90f84f0f9416388c950b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 11:35:48 +0200 Subject: [PATCH 16/23] Adjust expected error message to work for PHP compiled from PR branch See https://github.com/php/php-src/pull/15063/files#diff-4ffd79a0cf90a14777c4fe03429e25ec414ca63e8a4535319699bc119f52add0R58 --- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index a998a7dd..c65e4a66 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -151,7 +151,7 @@ public function can_be_assigned_via_reflection() { Assert::equals('Test', $i->fixture); } - #[Test, Expect(class: Error::class, message: '/Cannot initialize readonly property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot (initialize readonly|modify protected\(set\)) property .+fixture/')] public function cannot_initialize_from_outside() { $t= $this->declare('class %T { public readonly string $fixture; From 501c3d512058fb064ec5e6d3913f7ed4ec90822f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 12:32:21 +0200 Subject: [PATCH 17/23] Remove test for `public public(set)`, where the set hook is erased --- .../lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index d822373c..4b404a9d 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -96,7 +96,7 @@ public function rename() { $t->newInstance()->rename(); } - #[Test, Values(['private', 'protected', 'public'])] + #[Test, Values(['private', 'protected'])] public function reflection($modifier) { $t= $this->declare('class %T { public '.$modifier.'(set) string $fixture= "Test"; From d0e2732596637bda033480e968bc8d13f679c921 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 12:51:01 +0200 Subject: [PATCH 18/23] Fold declarations like `[visibility] [visibility](set)` to just the visibility itself --- .../ast/emit/AsymmetricVisibility.class.php | 23 ++++++++++++------- .../emit/AsymmetricVisibilityTest.class.php | 12 ++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 7692bf84..8daac83a 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -33,12 +33,26 @@ protected function emitProperty($result, $property) { 'private(set)' => 0x1000, ]; - // Emit XP meta information for the reflection API $scope= $result->codegen->scope[0]; $modifiers= 0; foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } + + // Declare checks for private(set) and protected(set), folding declarations + // like `[visibility] [visibility](set)` to just the visibility itself. + if ($modifiers & 0x0400) { + $checks= []; + $modifiers&= ~0x0400; + } else if ($modifiers & 0x0800) { + $checks= [$this->protected($property->name, 'modify protected(set)')]; + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0800; + } else if ($modifiers & 0x1000) { + $checks= [$this->private($property->name, 'modify private(set)')]; + $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x1000; + } + + // Emit XP meta information for the reflection API $scope->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, @@ -47,13 +61,6 @@ protected function emitProperty($result, $property) { DETAIL_ARGUMENTS => [$modifiers] ]; - $checks= []; - if ($modifiers & 0x1000) { - $checks[]= $this->private($property->name, 'modify private(set)'); - } else if ($modifiers & 0x0800) { - $checks[]= $this->protected($property->name, 'modify protected(set)'); - } - // The readonly flag is really two flags in one: write-once and restricted(set) if (in_array('readonly', $property->modifiers)) { $checks[]= $this->initonce($property->name); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 4b404a9d..ca67f06c 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -107,4 +107,16 @@ public function reflection($modifier) { $t->property('fixture')->toString() ); } + + #[Test, Values(['private', 'protected', 'public'])] + public function same_modifier_for_get_and_set($modifier) { + $t= $this->declare('class %T { + '.$modifier.' '.$modifier.'(set) string $fixture= "Test"; + }'); + + Assert::equals( + $modifier.' string $fixture', + $t->property('fixture')->toString() + ); + } } \ No newline at end of file From 28403bd4a41d3030f59d806355c979f0f22fdfa6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 13:50:57 +0200 Subject: [PATCH 19/23] Add support for emitting properties with asymmetric visibility natively --- .../lang/ast/emit/RewriteProperties.class.php | 8 +++++++- .../emit/AsymmetricVisibilityTest.class.php | 20 +++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index 094a8298..d0a50e81 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -1,5 +1,7 @@ hooks) { return $this->emitPropertyHooks($result, $property); - } else if (array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)'])) { + } else if ( + !($asymmetric ?? $asymmetric= method_exists(ReflectionProperty::class, 'isPrivateSet')) && + array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) + ) { return $this->emitAsymmetricVisibility($result, $property); } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index ca67f06c..1c327efe 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -13,7 +13,7 @@ class AsymmetricVisibilityTest extends EmittingTest { #[Test] public function reading() { $t= $this->declare('class %T { - public private(set) $fixture= "Test"; + public private(set) string $fixture= "Test"; }'); Assert::equals('Test', $t->newInstance()->fixture); } @@ -21,7 +21,7 @@ public function reading() { #[Test] public function writing_from_self_scope() { $t= $this->declare('class %T { - public private(set) $fixture= "Test"; + public private(set) string $fixture= "Test"; public function rename($name) { $this->fixture= $name; @@ -35,7 +35,7 @@ public function rename($name) { #[Test] public function writing_from_inherited_scope() { - $parent= $this->declare('class %T { public protected(set) $fixture= "Test"; }'); + $parent= $this->declare('class %T { public protected(set) string $fixture= "Test"; }'); $t= $this->declare('class %T extends '.$parent->literal().' { public function rename($name) { $this->fixture= $name; @@ -50,7 +50,7 @@ public function rename($name) { #[Test] public function writing_explicitely_public_set() { $t= $this->declare('class %T { - public public(set) $fixture= "Test"; + public public(set) string $fixture= "Test"; }'); $instance= $t->newInstance(); @@ -61,7 +61,7 @@ public function writing_explicitely_public_set() { #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { - public private(set) $fixture= "Test"; + public private(set) string $fixture= "Test"; }'); $t->newInstance()->fixture= 'Changed'; } @@ -69,7 +69,7 @@ public function writing_private() { #[Test, Expect(class: Error::class, message: '/Cannot modify protected\(set\) property T.+::\$fixture/')] public function writing_protected() { $t= $this->declare('class %T { - public protected(set) $fixture= "Test"; + public protected(set) string $fixture= "Test"; }'); $t->newInstance()->fixture= 'Changed'; } @@ -77,7 +77,7 @@ public function writing_protected() { #[Test] public function promoted_constructor_parameter() { $t= $this->declare('class %T { - public function __construct(public private(set) $fixture) { } + public function __construct(public private(set) string $fixture) { } }'); Assert::equals('Test', $t->newInstance('Test')->fixture); } @@ -87,7 +87,11 @@ public function readonly() { $t= $this->declare('class %T { // public-read, protected-write, write-once property - public protected(set) readonly string $fixture= "Test"; + public protected(set) readonly string $fixture; + + public function __construct() { + $this->fixture= "Test"; + } public function rename() { $this->fixture= "Changed"; // Will always error From 4a35832958195887b0a86f37f644aca7c9bae9a3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 21:32:47 +0200 Subject: [PATCH 20/23] Test against target version, not runtime version --- src/main/php/lang/ast/emit/PHP74.class.php | 2 ++ src/main/php/lang/ast/emit/PHP80.class.php | 2 ++ src/main/php/lang/ast/emit/PHP81.class.php | 2 ++ src/main/php/lang/ast/emit/PHP82.class.php | 2 ++ src/main/php/lang/ast/emit/PHP83.class.php | 2 ++ src/main/php/lang/ast/emit/RewriteProperties.class.php | 8 +++++--- 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 72a60b59..6f97959c 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -28,6 +28,8 @@ class PHP74 extends PHP { RewriteThrowableExpressions ; + public $targetVersion= 70400; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 5cd927b8..db83e745 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -31,6 +31,8 @@ class PHP80 extends PHP { RewriteStaticVariableInitializations ; + public $targetVersion= 80000; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index de5db7dc..f93b4969 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -28,6 +28,8 @@ class PHP81 extends PHP { OmitConstantTypes ; + public $targetVersion= 80100; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 8d0c1b96..d66d1016 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -28,6 +28,8 @@ class PHP82 extends PHP { OmitConstantTypes ; + public $targetVersion= 80200; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 7ae725f7..f33ef341 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -20,6 +20,8 @@ class PHP83 extends PHP { use RewriteBlockLambdaExpressions, RewriteProperties, ReadonlyClasses; + public $targetVersion= 80300; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index d0a50e81..3073aad9 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -10,15 +10,17 @@ trait RewriteProperties { } protected function emitProperty($result, $property) { - static $asymmetric= null; if ($property->hooks) { return $this->emitPropertyHooks($result, $property); } else if ( - !($asymmetric ?? $asymmetric= method_exists(ReflectionProperty::class, 'isPrivateSet')) && + $this->targetVersion < 80400 && array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) ) { return $this->emitAsymmetricVisibility($result, $property); - } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { + } else if ( + $this->targetVersion < 80100 && + in_array('readonly', $property->modifiers) + ) { return $this->emitReadonlyProperties($result, $property); } parent::emitProperty($result, $property); From 1db0df41cb69bc575771fcb6342ea17f627dc232 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 26 Aug 2024 21:25:23 +0200 Subject: [PATCH 21/23] Use xp-framework/reflection release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5dcb4c8d..16ea0d27 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", - "xp-framework/reflection": "dev-feature/asymmetric-visibility as 3.2.0", + "xp-framework/reflection": "^3.2", "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", "php" : ">=7.4.0" }, From e4a24c023dd85b3f620b3e2cc142bf33e9c136e2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 26 Aug 2024 21:30:08 +0200 Subject: [PATCH 22/23] Use xp-framework/ast release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 16ea0d27..60da5ba3 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", "xp-framework/reflection": "^3.2", - "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", + "xp-framework/ast": "^11.3", "php" : ">=7.4.0" }, "require-dev" : { From 31eac561a8ddd72c5f6e3eb62c3b6c499fcadad5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 20:25:52 +0200 Subject: [PATCH 23/23] Adjust modifier bits to reflection library See https://github.com/xp-framework/compiler/pull/183#discussion_r1733311942 --- .../ast/emit/AsymmetricVisibility.class.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 8daac83a..9bf53ce0 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -28,9 +28,9 @@ protected function emitProperty($result, $property) { 'final' => MODIFIER_FINAL, 'abstract' => MODIFIER_ABSTRACT, 'readonly' => MODIFIER_READONLY, - 'public(set)' => 0x0400, - 'protected(set)' => 0x0800, - 'private(set)' => 0x1000, + 'public(set)' => 0x1000000, + 'protected(set)' => 0x0000800, + 'private(set)' => 0x0001000, ]; $scope= $result->codegen->scope[0]; @@ -41,15 +41,15 @@ protected function emitProperty($result, $property) { // Declare checks for private(set) and protected(set), folding declarations // like `[visibility] [visibility](set)` to just the visibility itself. - if ($modifiers & 0x0400) { + if ($modifiers & 0x1000000) { $checks= []; - $modifiers&= ~0x0400; - } else if ($modifiers & 0x0800) { + $modifiers&= ~0x1000000; + } else if ($modifiers & 0x0000800) { $checks= [$this->protected($property->name, 'modify protected(set)')]; - $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0800; - } else if ($modifiers & 0x1000) { + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800; + } else if ($modifiers & 0x0001000) { $checks= [$this->private($property->name, 'modify private(set)')]; - $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x1000; + $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000; } // Emit XP meta information for the reflection API