Skip to content

Commit

Permalink
Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor (#…
Browse files Browse the repository at this point in the history
…504)

Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor
  • Loading branch information
samsonasik authored May 7, 2024
1 parent 452a023 commit e20f450
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 6 deletions.
30 changes: 24 additions & 6 deletions src/Instrument/Transformer/SelfValueVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\UnionType;
use PhpParser\NodeVisitorAbstract;
use UnexpectedValueException;

Expand Down Expand Up @@ -82,10 +85,6 @@ public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = !empty($node->name) ? $node->name->toString() : null;
} elseif ($node instanceof Class_) {
if ($node->name !== null) {
$this->className = new Name($node->name->toString());
}
} elseif ($node instanceof ClassMethod || $node instanceof Closure) {
if (isset($node->returnType)) {
$node->returnType = $this->resolveType($node->returnType);
Expand All @@ -107,6 +106,12 @@ public function enterNode(Node $node)
foreach ($node->types as &$type) {
$type = $this->resolveClassName($type);
}
} elseif ($node instanceof ClassLike) {
if (! $node instanceof Trait_) {
$this->className = !empty($node->name) ? new Name($node->name->toString()) : null;
} else {
$this->className = null;
}
}

return null;
Expand All @@ -126,6 +131,10 @@ protected function resolveClassName(Name $name): Name
return $name;
}

if ($this->className === null) {
return $name;
}

// Save the original name
$originalName = $name;
$name = clone $originalName;
Expand All @@ -142,7 +151,7 @@ protected function resolveClassName(Name $name): Name
/**
* Helper method for resolving type nodes
*
* @return NullableType|Name|FullyQualified|Identifier
* @return NullableType|Name|FullyQualified|Identifier|UnionType|IntersectionType
*/
private function resolveType(Node $node)
{
Expand All @@ -157,6 +166,15 @@ private function resolveType(Node $node)
return $node;
}

if ($node instanceof UnionType || $node instanceof IntersectionType) {
$types = [];
foreach ($node->types as $type) {
$types[] = $this->resolveType($type);
}
$node->types = $types;
return $node;
}

throw new UnexpectedValueException('Unknown node type: ' . get_class($node));
}
}
4 changes: 4 additions & 0 deletions tests/Go/Instrument/Transformer/SelfValueTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,9 @@ public static function filesDataProvider(): \Generator
__DIR__ . '/_files/php82-file.php',
__DIR__ . '/_files/php82-file-transformed.php'
];
yield 'anonymous-class.php' => [
__DIR__ . '/_files/anonymous-class.php',
__DIR__ . '/_files/anonymous-class-transformed.php'
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Parser Reflection API
*
* @copyright Copyright 2016, Lisachenko Alexander <[email protected]>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);

namespace Go\ParserReflection\Stub;

class InAnonymousClass
{
public function respond()
{
new class {
public const FOO = 'foo';

public function run()
{
return self::FOO;
}
};
}
}
27 changes: 27 additions & 0 deletions tests/Go/Instrument/Transformer/_files/anonymous-class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Parser Reflection API
*
* @copyright Copyright 2016, Lisachenko Alexander <[email protected]>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);

namespace Go\ParserReflection\Stub;

class InAnonymousClass
{
public function respond()
{
new class {
public const FOO = 'foo';

public function run()
{
return self::FOO;
}
};
}
}
131 changes: 131 additions & 0 deletions tests/Go/Instrument/Transformer/_files/php80-file-transformed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
/**
* Parser Reflection API
*
* @copyright Copyright 2016, Lisachenko Alexander <[email protected]>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);

namespace Go\ParserReflection\Stub;

use Attribute;
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};

class ClassWithPhp80Features
{
public function acceptsStringArrayDefaultToNull(array|string $iterable = null) : array {}
}

/**
* @see https://php.watch/versions/8.0/named-parameters
*/
class ClassWithPHP80NamedCall
{
public static function foo(string $key1 = '', string $key2 = ''): string
{
return $key1 . ':' . $key2;
}

public static function namedCall(): array
{
return [
'key1' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key1: 'bar'),
'key2' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key2: 'baz'),
'keys' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key1: 'A', key2: 'B'),
'reverseKeys' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key2: 'A', key1: 'B'),
'unpack' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(...['key1' => 'C', 'key2' => 'D']),
];
}
}

/**
* @see https://php.watch/versions/8.0/attributes
*/
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
readonly class ClassPHP80Attribute
{
private string $value;

public function __construct(string $value)
{
$this->value = $value;
}

public function getValue(): string
{
return $this->value;
}
}

/**
* @see https://php.watch/versions/8.0/attributes
*/
#[ClassPHP80Attribute('class')]
class ClassPHP80WithAttribute
{
#[ClassPHP80Attribute('first')]
#[ClassPHP80Attribute('second')]
public const PUBLIC_CONST = 1;

#[ClassPHP80Attribute('property')]
private string $privateProperty = 'foo';

#[ClassPHP80Attribute('method')]
public function bar(#[ClassPHP80Attribute('parameter')] $parameter)
{}
}

/**
* @see https://php.watch/versions/8.0/constructor-property-promotion
*/
class ClassPHP80WithPropertyPromotion
{
public function __construct(
private string $privateStringValue,
private $privateNonTypedValue,
protected int $protectedIntValue = 42,
public array $publicArrayValue = [M_PI, M_E],
) {}
}

/**
* @see https://php.watch/versions/8.0/union-types
*/
class ClassWithPHP80UnionTypes
{
public string|int|float|bool $scalarValue;

public array|object|null $complexValueOrNull = null;

/**
* Special case, internally iterable should be replaced with Traversable|array
*/
public iterable|object $iterableOrObject;

public static function returnsUnionType(): object|array|null {}

public static function acceptsUnionType(\stdClass|\Traversable|array $iterable): void {}
}

/**
* @see https://php.watch/versions/8.0/mixed-type
*/
class ClassWithPHP80MixedType
{
public mixed $someMixedPublicProperty;

public static function returnsMixed(): mixed {}

public static function acceptsMixed(mixed $value): void {}
}

/**
* @see https://php.watch/versions/8.0/static-return-type
*/
class ClassWithPHP80StaticReturnType
{
public static function create(): static {}
}
119 changes: 119 additions & 0 deletions tests/Go/Instrument/Transformer/_files/php81-file-transformed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
* Parser Reflection API
*
* @copyright Copyright 2024, Lisachenko Alexander <[email protected]>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);

namespace Go\ParserReflection\Stub;

/**
* @see https://php.watch/versions/8.1/readonly
*/
class ClassWithPhp81ReadOnlyProperties
{
public readonly int $publicReadonlyInt;

protected readonly array $protectedReadonlyArray;

private readonly object $privateReadonlyObject;
}

/**
* @see https://php.watch/versions/8.1/enums
*/
enum SimplePhp81EnumWithSuit {
case Clubs;
case Diamonds;
case Hearts;
case Spades;
}

/**
* @see https://php.watch/versions/8.1/enums#enums-backed
*/
enum BackedPhp81EnumHTTPMethods: string
{
case GET = 'get';
case POST = 'post';
}

/**
* @see https://php.watch/versions/8.1/enums#enum-methods
*/
enum BackedPhp81EnumHTTPStatusWithMethod: int
{
case OK = 200;
case ACCESS_DENIED = 403;
case NOT_FOUND = 404;

public function label(): string {
return static::getLabel($this);
}

public static function getLabel(\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod $value): string {
return match ($value) {
\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::OK => 'OK',
\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::ACCESS_DENIED => 'Access Denied',
\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::NOT_FOUND => 'Page Not Found',
};
}
}

/**
* @see https://php.watch/versions/8.1/intersection-types
*/
class ClassWithPhp81IntersectionType implements \Countable
{
private \Iterator&\Countable $countableIterator;

public function __construct(\Iterator&\Countable $countableIterator)
{
$this->countableIterator = $countableIterator;
}

public function count(): int
{
return count($this->countableIterator);
}
}

/**
* @see https://php.watch/versions/8.1/intersection-types
*/
function functionWithPhp81IntersectionType(\Iterator&\Countable $value): \Iterator&\Countable {
foreach($value as $val) {}
count($value);

return $value;
}

/**
* @see https://php.watch/versions/8.1/never-return-type
*/
class ClassWithPhp81NeverReturnType
{
public static function doThis(): never
{
throw new \RuntimeException('Not implemented');
}
}

/**
* @see https://php.watch/versions/8.1/never-return-type
*/
function functionWithPhp81NeverReturnType(): never
{
throw new \RuntimeException('Not implemented');
}

/**
* @see https://php.watch/versions/8.1/final-class-const
*/
class ClassWithPhp81FinalClassConst {
final public const TEST = '1';
}
Loading

0 comments on commit e20f450

Please sign in to comment.