Skip to content

Commit

Permalink
Merge pull request #43 from xp-framework/feature/asymmetric-visibility
Browse files Browse the repository at this point in the history
Add support for asymmetric visibility for properties
  • Loading branch information
thekid authored Aug 26, 2024
2 parents fab6891 + bea9794 commit fe96140
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 33 deletions.
99 changes: 72 additions & 27 deletions src/main/php/lang/reflection/Modifiers.class.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
<?php namespace lang\reflection;

use lang\Value;
use lang\{Value, IllegalArgumentException};

/**
* Type and member modifiers
*
* @test lang.reflection.unittest.ModifiersTest
*/
class Modifiers implements Value {
const IS_STATIC = MODIFIER_STATIC;
const IS_ABSTRACT = MODIFIER_ABSTRACT;
const IS_FINAL = MODIFIER_FINAL;
const IS_PUBLIC = MODIFIER_PUBLIC;
const IS_PROTECTED = MODIFIER_PROTECTED;
const IS_PRIVATE = MODIFIER_PRIVATE;
const IS_READONLY = 0x0080; // XP 10.13: MODIFIER_READONLY
const IS_NATIVE = 0xF000;
const IS_STATIC = MODIFIER_STATIC;
const IS_ABSTRACT = MODIFIER_ABSTRACT;
const IS_FINAL = MODIFIER_FINAL;
const IS_PUBLIC = MODIFIER_PUBLIC;
const IS_PROTECTED = MODIFIER_PROTECTED;
const IS_PRIVATE = MODIFIER_PRIVATE;
const IS_READONLY = MODIFIER_READONLY;
const IS_PUBLIC_SET = 0x0400;
const IS_PROTECTED_SET = 0x0800;
const IS_PRIVATE_SET = 0x1000;
const IS_NATIVE = 0x10000;

const GET_MASK = 0x0007; // PUBLIC | PROTECTED | PRIVATE
const SET_MASK = 0x1c00; // PUBLIC_SET | PROTECTED_SET | PRIVATE_SET

private static $names= [
'public' => self::IS_PUBLIC,
'protected' => self::IS_PROTECTED,
'private' => self::IS_PRIVATE,
'static' => self::IS_STATIC,
'final' => self::IS_FINAL,
'abstract' => self::IS_ABSTRACT,
'native' => self::IS_NATIVE,
'readonly' => self::IS_READONLY,
'public' => self::IS_PUBLIC,
'protected' => self::IS_PROTECTED,
'private' => self::IS_PRIVATE,
'static' => self::IS_STATIC,
'final' => self::IS_FINAL,
'abstract' => self::IS_ABSTRACT,
'native' => self::IS_NATIVE,
'readonly' => self::IS_READONLY,
'public(set)' => self::IS_PUBLIC_SET,
'protected(set)' => self::IS_PROTECTED_SET,
'private(set)' => self::IS_PRIVATE_SET,
];
private $bits;

Expand All @@ -45,7 +54,7 @@ public function __construct($arg= 0, $visibility= true) {
}

if ($visibility && 0 === ($this->bits & (self::IS_PROTECTED | self::IS_PRIVATE))) {
$this->bits |= self::IS_PUBLIC;
$this->bits|= self::IS_PUBLIC;
}
}

Expand All @@ -58,7 +67,7 @@ public function __construct($arg= 0, $visibility= true) {
private static function parse($names) {
$bits= 0;
foreach ($names as $name) {
$bits |= self::$names[$name];
$bits|= self::$names[$name];
}
return $bits;
}
Expand Down Expand Up @@ -103,19 +112,55 @@ public function isAbstract() { return 0 !== ($this->bits & self::IS_ABSTRACT); }
public function isFinal() { return 0 !== ($this->bits & self::IS_FINAL); }

/** @return bool */
public function isPublic() { return 0 !== ($this->bits & self::IS_PUBLIC); }
public function isNative() { return 0 !== ($this->bits & self::IS_NATIVE); }

/** @return bool */
public function isProtected() { return 0 !== ($this->bits & self::IS_PROTECTED); }
public function isReadonly() { return 0 !== ($this->bits & self::IS_READONLY); }

/** @return bool */
public function isPrivate() { return 0 !== ($this->bits & self::IS_PRIVATE); }
/**
* Gets whether these modifiers are public in regard to the specified hook
*
* @param string $hook
* @return bool
* @throws lang.IllegalArgumentException
*/
public function isPublic($hook= 'get') {
switch ($hook) {
case 'get': return 0 !== ($this->bits & self::IS_PUBLIC);
case 'set': return 0 !== ($this->bits & self::IS_PUBLIC_SET);
default: throw new IllegalArgumentException('Unknown hook '.$hook);
}
}

/** @return bool */
public function isNative() { return 0 !== ($this->bits & self::IS_NATIVE); }
/**
* Gets whether these modifiers are protected in regard to the specified hook
*
* @param string $hook
* @return bool
* @throws lang.IllegalArgumentException
*/
public function isProtected($hook= 'get') {
switch ($hook) {
case 'get': return 0 !== ($this->bits & self::IS_PROTECTED);
case 'set': return 0 !== ($this->bits & self::IS_PROTECTED_SET);
default: throw new IllegalArgumentException('Unknown hook '.$hook);
}
}

/** @return bool */
public function isReadonly() { return 0 !== ($this->bits & self::IS_READONLY); }
/**
* Gets whether these modifiers are private in regard to the specified hook
*
* @param string $hook
* @return bool
* @throws lang.IllegalArgumentException
*/
public function isPrivate($hook= 'get') {
switch ($hook) {
case 'get': return 0 !== ($this->bits & self::IS_PRIVATE);
case 'set': return 0 !== ($this->bits & self::IS_PRIVATE_SET);
default: throw new IllegalArgumentException('Unknown hook '.$hook);
}
}

/**
* Compares a given value to this modifiers instance
Expand Down
32 changes: 30 additions & 2 deletions src/main/php/lang/reflection/Property.class.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php namespace lang\reflection;

use ReflectionException, ReflectionUnionType, Throwable;
use lang\{Reflection, XPClass, Type, VirtualProperty, TypeUnion};
use lang\{Reflection, XPClass, Type, VirtualProperty, TypeUnion, IllegalArgumentException};

/**
* Reflection for a single property
Expand Down Expand Up @@ -38,6 +38,32 @@ public function constraint() {
return new Constraint($t ?? Type::$VAR, $present);
}

/**
* Gets whether these modifiers are public in regard to the specified hook
*
* @param ?string $hook Optionally, filter for specified hook only
* @return lang.reflection.Modifiers
* @throws lang.IllegalArgumentException
*/
public function modifiers($hook= null) {
static $set= [
Modifiers::IS_PUBLIC_SET => Modifiers::IS_PUBLIC,
Modifiers::IS_PROTECTED_SET => Modifiers::IS_PROTECTED,
Modifiers::IS_PRIVATE_SET => Modifiers::IS_PRIVATE,
];

// Readonly implies protected(set)
$bits= $this->reflect->getModifiers();
$bits & Modifiers::IS_READONLY && $bits|= Modifiers::IS_PROTECTED_SET;

switch ($hook) {
case null: return new Modifiers($bits);
case 'get': return new Modifiers(($bits & ~Modifiers::SET_MASK) & Modifiers::GET_MASK);
case 'set': return new Modifiers($set[$bits & Modifiers::SET_MASK] ?? $bits & Modifiers::GET_MASK);
default: throw new IllegalArgumentException('Unknown hook '.$hook);
}
}

/**
* Gets this property's value
*
Expand Down Expand Up @@ -100,7 +126,9 @@ public function toString() {
$name= $t->getName();
}

return Modifiers::namesOf($this->reflect->getModifiers()).' '.$name.' $'.$this->reflect->getName();
$bits= $this->reflect->getModifiers();
$bits & Modifiers::IS_READONLY && $bits|= Modifiers::IS_PROTECTED_SET;
return Modifiers::namesOf($bits).' '.$name.' $'.$this->reflect->getName();
}

/**
Expand Down
33 changes: 33 additions & 0 deletions src/test/php/lang/reflection/unittest/ModifiersTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ private function cases() {
yield [Modifiers::IS_PRIVATE, 'private'];
yield [Modifiers::IS_NATIVE, 'native'];
yield [Modifiers::IS_READONLY, 'readonly'];
yield [Modifiers::IS_PRIVATE_SET, 'private(set)'];
yield [Modifiers::IS_PROTECTED_SET, 'protected(set)'];
yield [Modifiers::IS_PUBLIC_SET, 'public(set)'];
yield [Modifiers::IS_FINAL | Modifiers::IS_PUBLIC, 'public final'];
yield [Modifiers::IS_ABSTRACT | Modifiers::IS_PUBLIC, 'public abstract'];
yield [Modifiers::IS_ABSTRACT | Modifiers::IS_PROTECTED, 'protected abstract'];
Expand Down Expand Up @@ -83,6 +86,36 @@ public function isReadonly($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isReadonly());
}

#[Test, Values([['public(set)', true], ['public', true]])]
public function isPublicGet($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isPublic('get'));
}

#[Test, Values([['protected(set)', false], ['protected', true]])]
public function isProtectedGet($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isProtected('get'));
}

#[Test, Values([['private(set)', false], ['private', true]])]
public function isPrivateGet($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isPrivate('get'));
}

#[Test, Values([['public(set)', true], ['public', false]])]
public function isPublicSet($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isPublic('set'));
}

#[Test, Values([['protected(set)', true], ['protected', false]])]
public function isProtectedSet($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isProtected('set'));
}

#[Test, Values([['private(set)', true], ['private', false]])]
public function isPrivateSet($input, $expected) {
Assert::equals($expected, (new Modifiers($input))->isPrivate('set'));
}

#[Test]
public function public_modifier_default_no_arg() {
Assert::true((new Modifiers())->isPublic());
Expand Down
82 changes: 80 additions & 2 deletions src/test/php/lang/reflection/unittest/PropertiesTest.class.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<?php namespace lang\reflection\unittest;

use ReflectionProperty;
use lang\reflection\{AccessingFailed, CannotAccess, Constraint, Modifiers};
use lang\{Primitive, Type, TypeIntersection, TypeUnion, XPClass, IllegalStateException};
use test\verify\Runtime;
use lang\{Primitive, Type, TypeIntersection, TypeUnion, XPClass, IllegalArgumentException};
use test\verify\{Condition, Runtime};
use test\{Action, Assert, Expect, Test, Values};

class PropertiesTest {
use TypeDefinition;

private static $ASYMMETRIC;

static function __static() {
self::$ASYMMETRIC= method_exists(ReflectionProperty::class, 'isPrivateSet');
}

#[Test]
public function name() {
Assert::equals('fixture', $this->declare('{ public $fixture; }')->property('fixture')->name());
Expand All @@ -27,6 +34,43 @@ public function modifiers() {
);
}

#[Test, Values(['public', 'protected', 'private'])]
public function get_modifiers($modifier) {
Assert::equals(
new Modifiers($modifier),
$this->declare('{ '.$modifier.' $fixture; }')->property('fixture')->modifiers('get')
);
}

#[Test, Values(['public', 'protected', 'private'])]
public function set_modifiers($modifier) {
Assert::equals(
new Modifiers($modifier),
$this->declare('{ '.$modifier.' $fixture; }')->property('fixture')->modifiers('set')
);
}

#[Test]
public function get_modifiers_erases_static() {
Assert::equals(
new Modifiers('public'),
$this->declare('{ public static int $fixture; }')->property('fixture')->modifiers('get')
);
}

#[Test]
public function set_modifiers_erases_static() {
Assert::equals(
new Modifiers('public'),
$this->declare('{ public static int $fixture; }')->property('fixture')->modifiers('set')
);
}

#[Test, Expect(IllegalArgumentException::class)]
public function modifiers_unknown_hook() {
$this->declare('{ private $fixture; }')->property('fixture')->modifiers('@unknown');
}

#[Test]
public function no_comment() {
Assert::null($this->declare('{ private $fixture; }')->property('fixture')->comment());
Expand Down Expand Up @@ -227,4 +271,38 @@ public function set_accessing_failed_exceptions_target_member() {
Assert::equals($t->property('fixture'), $expected->target());
}
}

#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public protected(set)', 'public private(set)', 'protected private(set)'])]
public function asymmetric_visibility($modifiers) {
$t= $this->declare('{ '.$modifiers.' int $fixture; }');
Assert::equals(
$modifiers.' int $fixture',
$t->property('fixture')->toString()
);
}

#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public', 'protected', 'private'])]
public function set_implicit_when_same_as_get($modifier) {
$t= $this->declare('{ '.$modifier.' '.$modifier.'(set) int $fixture; }');
Assert::equals(
$modifier.' int $fixture',
$t->property('fixture')->toString()
);
}

#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public', 'protected', 'private'])]
public function asymmetric_get($modifier) {
Assert::equals(
new Modifiers($modifier),
$this->declare('{ '.$modifier.' private(set) int $fixture; }')->property('fixture')->modifiers('get')
);
}

#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public', 'protected', 'private'])]
public function asymmetric_set($modifier) {
Assert::equals(
new Modifiers($modifier),
$this->declare('{ public '.$modifier.'(set) int $fixture; }')->property('fixture')->modifiers('set')
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ private function fixtures() {

#[Test, Values(from: 'fixtures')]
public function readonly_modifier_shown_in_string_representation($type) {
Assert::equals('public readonly string $fixture', $type->property('fixture')->toString());
Assert::equals('public readonly protected(set) string $fixture', $type->property('fixture')->toString());
}

#[Test, Values(from: 'fixtures')]
public function virtual_property_included_in_list($type) {
Assert::equals(
['fixture' => 'public readonly'],
['fixture' => 'public readonly protected(set)'],
array_map(fn($p) => $p->modifiers()->names(), iterator_to_array($type->properties()))
);
}
Expand Down

0 comments on commit fe96140

Please sign in to comment.