Skip to content

Commit

Permalink
Factory & Extractor: added support for property hooks & asymmetric vi…
Browse files Browse the repository at this point in the history
…sibility
  • Loading branch information
dg committed Nov 29, 2024
1 parent 9fa3236 commit d201c9b
Show file tree
Hide file tree
Showing 7 changed files with 623 additions and 11 deletions.
66 changes: 56 additions & 10 deletions src/PhpGenerator/Extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use Nette;
use PhpParser;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\ParserFactory;
Expand Down Expand Up @@ -323,14 +324,37 @@ private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node):
foreach ($node->props as $item) {
$prop = $class->addProperty($item->name->toString());
$prop->setStatic($node->isStatic());
$prop->setVisibility($this->toVisibility($node->flags));
$prop->setVisibility($this->toVisibility($node->flags), $this->toSetterVisibility($node->flags));
$prop->setType($node->type ? $this->toPhp($node->type) : null);
if ($item->default) {
$prop->setValue($this->toValue($item->default));
}

$prop->setReadOnly((method_exists($node, 'isReadonly') && $node->isReadonly()) || ($class instanceof ClassType && $class->isReadOnly()));
$this->addCommentAndAttributes($prop, $node);

$prop->setAbstract((bool) ($node->flags & Node\Stmt\Class_::MODIFIER_ABSTRACT));
$prop->setFinal((bool) ($node->flags & Node\Stmt\Class_::MODIFIER_FINAL));
$this->addHooksToProperty($prop, $node);
}
}


private function addHooksToProperty(Property|PromotedParameter $prop, Node\Stmt\Property|Node\Param $node): void
{
if (!class_exists(Node\PropertyHook::class)) {
return;
}

foreach ($node->hooks as $hookNode) {
$hook = $prop->addHook($hookNode->name->toString());
$hook->setFinal((bool) ($hookNode->flags & Modifiers::FINAL));
$this->setupFunction($hook, $hookNode);
if ($hookNode->body === null) {
$hook->setAbstract();
} elseif (!is_array($hookNode->body)) {
$hook->setBody($this->getReformattedContents([$hookNode->body], 1), short: true);
}
}
}

Expand Down Expand Up @@ -380,7 +404,7 @@ private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node):


private function addCommentAndAttributes(
PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse $element,
PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse|PropertyHook $element,
Node $node,
): void
{
Expand Down Expand Up @@ -408,19 +432,29 @@ private function addCommentAndAttributes(
}


private function setupFunction(GlobalFunction|Method $function, Node\FunctionLike $node): void
private function setupFunction(GlobalFunction|Method|PropertyHook $function, Node\FunctionLike $node): void
{
$function->setReturnReference($node->returnsByRef());
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
if (!$function instanceof PropertyHook) {
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
}

foreach ($node->getParams() as $item) {
$visibility = $this->toVisibility($item->flags);
$isReadonly = (bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY);
$param = $visibility
? ($function->addPromotedParameter($item->var->name))->setVisibility($visibility)->setReadonly($isReadonly)
: $function->addParameter($item->var->name);
$getVisibility = $this->toVisibility($item->flags);
$setVisibility = $this->toSetterVisibility($item->flags);
if ($getVisibility || $setVisibility) {
$param = $function->addPromotedParameter($item->var->name)
->setVisibility($getVisibility, $setVisibility)
->setReadonly((bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY));
$this->addHooksToProperty($param, $item);
} else {
$param = $function->addParameter($item->var->name);
}
$param->setType($item->type ? $this->toPhp($item->type) : null);
$param->setReference($item->byRef);
$function->setVariadic($item->variadic);
if (!$function instanceof PropertyHook) {
$function->setVariadic($item->variadic);
}
if ($item->default) {
$param->setDefaultValue($this->toValue($item->default));
}
Expand Down Expand Up @@ -491,6 +525,18 @@ private function toVisibility(int $flags): ?string
}


private function toSetterVisibility(int $flags): ?string
{
return match (true) {
!class_exists(Node\PropertyHook::class) => null,
(bool) ($flags & Modifiers::PUBLIC_SET) => Visibility::Public,
(bool) ($flags & Modifiers::PROTECTED_SET) => Visibility::Protected,
(bool) ($flags & Modifiers::PRIVATE_SET) => Visibility::Private,
default => null,
};
}


private function toPhp(Node $value): string
{
$dolly = clone $value;
Expand Down
46 changes: 45 additions & 1 deletion src/PhpGenerator/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public function fromParameterReflection(\ReflectionParameter $from): Parameter
$param = (new PromotedParameter($from->name))
->setVisibility($this->getVisibility($property))
->setReadOnly(PHP_VERSION_ID >= 80100 && $property->isReadonly());
$this->addHooks($property, $param);
} else {
$param = new Parameter($from->name);
}
Expand Down Expand Up @@ -260,15 +261,58 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
$prop->setStatic($from->isStatic());
$prop->setVisibility($this->getVisibility($from));
$prop->setType((string) $from->getType());

$prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults));
$prop->setReadOnly(PHP_VERSION_ID >= 80100 && $from->isReadOnly());
$prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
$prop->setAttributes($this->getAttributes($from));

if (PHP_VERSION_ID >= 80400) {
$this->addHooks($from, $prop);
$isInterface = $from->getDeclaringClass()->isInterface();
$prop->setFinal($from->isFinal() && !$prop->isPrivate(PropertyAccessMode::Set));
$prop->setAbstract($from->isAbstract() && !$isInterface);
}
return $prop;
}


private function addHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void
{
if (PHP_VERSION_ID < 80400) {
return;
}

$getV = $this->getVisibility($from);
$setV = $from->isPrivateSet()
? Visibility::Private
: ($from->isProtectedSet() ? Visibility::Protected : $getV);
$defaultSetV = $from->isReadOnly() && $getV !== Visibility::Private
? Visibility::Protected
: $getV;
if ($setV !== $defaultSetV) {
$prop->setVisibility($getV === Visibility::Public ? null : $getV, $setV);
}

foreach ($from->getHooks() as $type => $hook) {
$params = $hook->getParameters();
if (
count($params) === 1
&& $params[0]->getName() === 'value'
&& $params[0]->getType() == $from->getType() // intentionally ==
) {
$params = [];
}
$prop->addHook($type)
->setParameters(array_map([$this, 'fromParameterReflection'], $params))
->setAbstract($hook->isAbstract())
->setFinal($hook->isFinal())
->setReturnReference($hook->returnsReference())
->setComment(Helpers::unformatDocComment((string) $hook->getDocComment()))
->setAttributes($this->getAttributes($hook));
}
}


public function fromObject(object $obj): Literal
{
return new Literal('new \\' . $obj::class . '(/* unknown */)');
Expand Down
22 changes: 22 additions & 0 deletions tests/PhpGenerator/ClassType.from.84.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @phpVersion 8.4
*/

declare(strict_types=1);

use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\InterfaceType;

require __DIR__ . '/../bootstrap.php';
require __DIR__ . '/fixtures/classes.84.php';

$res[] = ClassType::from(Abc\PropertyHookSignatures::class);
$res[] = ClassType::from(Abc\AbstractHookSignatures::class);
$res[] = InterfaceType::from(Abc\InterfaceHookSignatures::class);
$res[] = ClassType::from(Abc\AsymmetricVisibilitySignatures::class);
$res[] = ClassType::from(Abc\CombinedSignatures::class);
$res[] = ClassType::from(Abc\ConstructorAllSignatures::class);

sameFile(__DIR__ . '/expected/ClassType.from.84.expect', implode("\n", $res));
5 changes: 5 additions & 0 deletions tests/PhpGenerator/Extractor.extractAll.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ sameFile(__DIR__ . '/expected/Extractor.classes.81.expect', (string) $file);
$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.82.php')))->extractAll();
sameFile(__DIR__ . '/expected/Extractor.classes.82.expect', (string) $file);

if (class_exists(PhpParser\Node\PropertyHook::class)) {
$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.84.php')))->extractAll();
sameFile(__DIR__ . '/expected/Extractor.classes.84.expect', (string) $file);
}

$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/enum.php')))->extractAll();
sameFile(__DIR__ . '/expected/Extractor.enum.expect', (string) $file);

Expand Down
163 changes: 163 additions & 0 deletions tests/PhpGenerator/expected/ClassType.from.84.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
class PropertyHookSignatures
{
public string $basic {
get {
}
}

public string $fullGet {
get {
}
}

protected string $refGet {
&get {
}
}

protected string $finalGet {
final get {
}
}

public string $basicSet {
set {
}
}

public string $fullSet {
set {
}
}

public string $setWithParam {
set(string $foo) {
}
}

public string $setWithParam2 {
set(string|int $value) {
}
}

public string $finalSet {
final set {
}
}

public string $combined {
set {
}
get {
}
}

final public string $combinedFinal {
/** comment set */
#[Set]
set {
}
/** comment get */
#[Get]
get {
}
}

public string $virtualProp {
set {
}
&get {
}
}
}

abstract class AbstractHookSignatures
{
abstract public string $abstractGet { get; }
abstract protected string $abstractSet { set; }
abstract public string $abstractBoth { set; get; }

abstract public string $mixedGet {
set {
}
get;
}

abstract public string $mixedSet {
set;
get {
}
}
}

interface InterfaceHookSignatures
{
public string $get { get; }

public string $set { #[Set]
set; }
public string $both { set; get; }
public string $refGet { &get; }
}

class AsymmetricVisibilitySignatures
{
private(set) string $first;
protected(set) string $second;
protected private(set) string $third;
private(set) string $fourth;
protected(set) string $fifth;
public readonly string $implicit;
private(set) readonly string $readFirst;
private(set) readonly string $readSecond;
protected readonly string $readThird;
public(set) readonly string $readFourth;
private(set) string $firstFinal;
final protected(set) string $secondFinal;
protected private(set) string $thirdFinal;
private(set) string $fourthFinal;
final protected(set) string $fifthFinal;
}

class CombinedSignatures
{
protected(set) string $prop2 {
final set {
}
get {
}
}

protected private(set) string $prop3 {
set {
}
final get {
}
}
}

class ConstructorAllSignatures
{
public function __construct(
private(set) string $prop1,
protected(set) string $prop2,
protected private(set) string $prop3,
private(set) string $prop4,
protected(set) string $prop5,
private(set) readonly string $readProp1,
private(set) readonly string $readProp2,
protected readonly string $readProp3,
public(set) readonly string $readProp4,
public string $hookProp1 {
get {
}
},
protected(set) string $mixedProp1 {
set {
}
get {
}
},
) {
}
}
Loading

0 comments on commit d201c9b

Please sign in to comment.