Skip to content

Commit f121e86

Browse files
authored
Merge pull request #183 from xp-framework/feature/asymmetric-visibility
Add emitting support for asymmetric visibility
2 parents f55a373 + c69c867 commit f121e86

13 files changed

+299
-44
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"require" : {
99
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
1010
"xp-framework/reflection": "^3.2 | ^2.15",
11-
"xp-framework/ast": "^11.1",
11+
"xp-framework/ast": "^11.3",
1212
"php" : ">=7.4.0"
1313
},
1414
"require-dev" : {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\nodes\{
4+
Assignment,
5+
Block,
6+
InstanceExpression,
7+
Literal,
8+
OffsetExpression,
9+
ReturnStatement,
10+
Variable
11+
};
12+
13+
/**
14+
* Asymmetric Visibility
15+
*
16+
* @see https://wiki.php.net/rfc/asymmetric-visibility-v2
17+
* @test lang.ast.unittest.emit.AsymmetricVisibilityTest
18+
*/
19+
trait AsymmetricVisibility {
20+
use VisibilityChecks;
21+
22+
protected function emitProperty($result, $property) {
23+
static $lookup= [
24+
'public' => MODIFIER_PUBLIC,
25+
'protected' => MODIFIER_PROTECTED,
26+
'private' => MODIFIER_PRIVATE,
27+
'static' => MODIFIER_STATIC,
28+
'final' => MODIFIER_FINAL,
29+
'abstract' => MODIFIER_ABSTRACT,
30+
'readonly' => MODIFIER_READONLY,
31+
'public(set)' => 0x1000000,
32+
'protected(set)' => 0x0000800,
33+
'private(set)' => 0x0001000,
34+
];
35+
36+
$scope= $result->codegen->scope[0];
37+
$modifiers= 0;
38+
foreach ($property->modifiers as $name) {
39+
$modifiers|= $lookup[$name];
40+
}
41+
42+
// Declare checks for private(set) and protected(set), folding declarations
43+
// like `[visibility] [visibility](set)` to just the visibility itself.
44+
if ($modifiers & 0x1000000) {
45+
$checks= [];
46+
$modifiers&= ~0x1000000;
47+
} else if ($modifiers & 0x0000800) {
48+
$checks= [$this->protected($property->name, 'modify protected(set)')];
49+
$modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800;
50+
} else if ($modifiers & 0x0001000) {
51+
$checks= [$this->private($property->name, 'modify private(set)')];
52+
$modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000;
53+
}
54+
55+
// Emit XP meta information for the reflection API
56+
$scope->meta[self::PROPERTY][$property->name]= [
57+
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
58+
DETAIL_ANNOTATIONS => $property->annotations,
59+
DETAIL_COMMENT => $property->comment,
60+
DETAIL_TARGET_ANNO => [],
61+
DETAIL_ARGUMENTS => [$modifiers]
62+
];
63+
64+
// The readonly flag is really two flags in one: write-once and restricted(set)
65+
if (in_array('readonly', $property->modifiers)) {
66+
$checks[]= $this->initonce($property->name);
67+
}
68+
69+
$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(
70+
new Literal('__virtual'),
71+
new Literal("'{$property->name}'"))
72+
);
73+
$scope->virtual[$property->name]= [
74+
new ReturnStatement($virtual),
75+
new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]),
76+
];
77+
if (isset($property->expression)) {
78+
$scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression;
79+
}
80+
}
81+
}

src/main/php/lang/ast/emit/PHP74.class.php

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class PHP74 extends PHP {
2828
RewriteThrowableExpressions
2929
;
3030

31+
public $targetVersion= 70400;
32+
3133
/** Sets up type => literal mappings */
3234
public function __construct() {
3335
$this->literals= [

src/main/php/lang/ast/emit/PHP80.class.php

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class PHP80 extends PHP {
3131
RewriteStaticVariableInitializations
3232
;
3333

34+
public $targetVersion= 80000;
35+
3436
/** Sets up type => literal mappings */
3537
public function __construct() {
3638
$this->literals= [

src/main/php/lang/ast/emit/PHP81.class.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ class PHP81 extends PHP {
2323
RewriteBlockLambdaExpressions,
2424
RewriteDynamicClassConstants,
2525
RewriteStaticVariableInitializations,
26+
RewriteProperties,
2627
ReadonlyClasses,
27-
OmitConstantTypes,
28-
PropertyHooks
28+
OmitConstantTypes
2929
;
3030

31+
public $targetVersion= 80100;
32+
3133
/** Sets up type => literal mappings */
3234
public function __construct() {
3335
$this->literals= [

src/main/php/lang/ast/emit/PHP82.class.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ class PHP82 extends PHP {
2323
RewriteBlockLambdaExpressions,
2424
RewriteDynamicClassConstants,
2525
RewriteStaticVariableInitializations,
26+
RewriteProperties,
2627
ReadonlyClasses,
27-
OmitConstantTypes,
28-
PropertyHooks
28+
OmitConstantTypes
2929
;
3030

31+
public $targetVersion= 80200;
32+
3133
/** Sets up type => literal mappings */
3234
public function __construct() {
3335
$this->literals= [

src/main/php/lang/ast/emit/PHP83.class.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
* @see https://wiki.php.net/rfc#php_83
1919
*/
2020
class PHP83 extends PHP {
21-
use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks;
21+
use RewriteBlockLambdaExpressions, RewriteProperties, ReadonlyClasses;
22+
23+
public $targetVersion= 80300;
2224

2325
/** Sets up type => literal mappings */
2426
public function __construct() {

src/main/php/lang/ast/emit/PropertyHooks.class.php

+3-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<?php namespace lang\ast\emit;
22

33
use ReflectionProperty;
4-
use lang\ast\Code;
54
use lang\ast\nodes\{
65
Assignment,
76
Block,
@@ -24,6 +23,7 @@
2423
* @test lang.ast.unittest.emit.PropertyHooksTest
2524
*/
2625
trait PropertyHooks {
26+
use VisibilityChecks;
2727

2828
protected function rewriteHook($node, $name, $virtual, $literal) {
2929

@@ -61,24 +61,14 @@ protected function rewriteHook($node, $name, $virtual, $literal) {
6161

6262
protected function withScopeCheck($modifiers, $nodes) {
6363
if ($modifiers & MODIFIER_PRIVATE) {
64-
$check= (
65-
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
66-
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
67-
'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);'
68-
);
64+
return new Block([$this->private('$name', 'access private'), ...$nodes]);
6965
} else if ($modifiers & MODIFIER_PROTECTED) {
70-
$check= (
71-
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
72-
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
73-
'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);'
74-
);
66+
return new Block([$this->protected('$name', 'access protected'), ...$nodes]);
7567
} else if (1 === sizeof($nodes)) {
7668
return $nodes[0];
7769
} else {
7870
return new Block($nodes);
7971
}
80-
81-
return new Block([new Code($check), ...$nodes]);
8272
}
8373

8474
protected function emitEmulatedHooks($result, $property) {

src/main/php/lang/ast/emit/ReadonlyProperties.class.php

+33-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
<?php namespace lang\ast\emit;
22

3+
use lang\ast\nodes\{
4+
Assignment,
5+
Block,
6+
InstanceExpression,
7+
InvokeExpression,
8+
Literal,
9+
OffsetExpression,
10+
ReturnStatement,
11+
Variable
12+
};
313
use lang\ast\{Code, Error, Errors};
414

515
/**
@@ -9,6 +19,7 @@
919
* @see https://wiki.php.net/rfc/readonly_properties_v2
1020
*/
1121
trait ReadonlyProperties {
22+
use VisibilityChecks;
1223

1324
protected function emitProperty($result, $property) {
1425
static $lookup= [
@@ -37,33 +48,33 @@ protected function emitProperty($result, $property) {
3748
];
3849

3950
// Add visibility check for accessing private and protected properties
40-
if (in_array('private', $property->modifiers)) {
41-
$check= (
42-
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
43-
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
44-
'throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");'
45-
);
46-
} else if (in_array('protected', $property->modifiers)) {
47-
$check= (
48-
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
49-
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
50-
'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");'
51-
);
51+
if ($modifiers & MODIFIER_PRIVATE) {
52+
$check= $this->private($property->name, 'access private');
53+
} else if ($modifiers & MODIFIER_PROTECTED) {
54+
$check= $this->protected($property->name, 'access protected');
5255
} else {
53-
$check= '';
56+
$check= null;
5457
}
5558

59+
$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(
60+
new Literal('__virtual'),
61+
new Literal("'{$property->name}'"))
62+
);
63+
5664
// Create virtual property implementing the readonly semantics
5765
$scope->virtual[$property->name]= [
58-
new Code(sprintf($check.'return $this->__virtual["%1$s"][0];', $property->name)),
59-
new Code(sprintf(
60-
($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;').
61-
'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'.
62-
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
63-
'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'.
64-
'$this->__virtual["%1$s"]= [$value];',
65-
$property->name
66-
)),
66+
$check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual),
67+
new Block([
68+
$check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'),
69+
$this->initonce($property->name),
70+
new Code(sprintf(
71+
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
72+
'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'.
73+
'$this->__virtual["%1$s"]= [$value];',
74+
$property->name
75+
)),
76+
new Assignment($virtual, '=', new Variable('value'))
77+
]),
6778
];
6879
}
6980
}

src/main/php/lang/ast/emit/RewriteProperties.class.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
<?php namespace lang\ast\emit;
22

3+
use ReflectionProperty;
4+
35
trait RewriteProperties {
4-
use PropertyHooks, ReadonlyProperties {
6+
use PropertyHooks, ReadonlyProperties, AsymmetricVisibility {
57
PropertyHooks::emitProperty as emitPropertyHooks;
68
ReadonlyProperties::emitProperty as emitReadonlyProperties;
9+
AsymmetricVisibility::emitProperty as emitAsymmetricVisibility;
710
}
811

912
protected function emitProperty($result, $property) {
1013
if ($property->hooks) {
1114
return $this->emitPropertyHooks($result, $property);
12-
} else if (in_array('readonly', $property->modifiers)) {
15+
} else if (
16+
$this->targetVersion < 80400 &&
17+
array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)'])
18+
) {
19+
return $this->emitAsymmetricVisibility($result, $property);
20+
} else if (
21+
$this->targetVersion < 80100 &&
22+
in_array('readonly', $property->modifiers)
23+
) {
1324
return $this->emitReadonlyProperties($result, $property);
1425
}
1526
parent::emitProperty($result, $property);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\Code;
4+
5+
trait VisibilityChecks {
6+
7+
private function initonce($name) {
8+
return new Code('if (isset($this->__virtual["'.$name.'"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::\$'.$name.'");');
9+
}
10+
11+
private function private($name, $access) {
12+
return new Code(
13+
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
14+
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
15+
'throw new \\Error("Cannot '.$access.' property ".__CLASS__."::\$'.$name.' from ".($scope ? "scope ".$scope : "global scope"));'
16+
);
17+
}
18+
19+
private function protected($name, $access) {
20+
return new Code(
21+
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
22+
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
23+
'throw new \\Error("Cannot '.$access.' property ".__CLASS__."::\$'.$name.' from ".($scope ? "scope ".$scope : "global scope"));'
24+
);
25+
}
26+
}

0 commit comments

Comments
 (0)