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
  • Loading branch information
samsonasik committed May 5, 2024
1 parent 452a023 commit 9f33694
Show file tree
Hide file tree
Showing 4 changed files with 357 additions and 1 deletion.
14 changes: 13 additions & 1 deletion src/Instrument/Transformer/SelfValueVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand All @@ -28,6 +29,8 @@
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 @@ -142,7 +145,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 +160,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));
}
}
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\ClassWithPhp81ReadOnlyProperties $value): string {
return match ($value) {
\Go\ParserReflection\Stub\ClassWithPhp81ReadOnlyProperties::OK => 'OK',
\Go\ParserReflection\Stub\ClassWithPhp81ReadOnlyProperties::ACCESS_DENIED => 'Access Denied',
\Go\ParserReflection\Stub\ClassWithPhp81ReadOnlyProperties::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';
}
94 changes: 94 additions & 0 deletions tests/Go/Instrument/Transformer/_files/php82-file-transformed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?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;

/**
* @see https://php.watch/versions/8.2/readonly-classes
*/
readonly class ClassWithPhp82ReadOnlyFlag
{
public int $publicInt;
}

/**
* @see https://php.watch/versions/8.2/dnf-types
*/
class ClassWithPhp82DNFType
{
private (JSONResponse&SuccessResponse)|HTMLResponse|string $respond;

public function __construct((JSONResponse&SuccessResponse)|HTMLResponse|string $respond)
{
$this->respond = $respond;
}

public function respond(): (JSONResponse&SuccessResponse)|HTMLResponse|string
{
return $this->respond;
}
}

/**
* @see https://php.watch/versions/8.2/null-false-types
* @see https://php.watch/versions/8.2/true-type
*/
class ClassWithPhp82NullFalseTypes
{
private true $isTrue = true;
private false $isFalse = false;
private null $isNull = null;

public function returnsFalse(): false
{
return false;
}

public function returnsTrue(): true
{
return true;
}

public function returnsNullExplicitly(): null
{
return null;
}

public function acceptsTrue(true $acceptsTrue): void {}
public function acceptsFalse(false $acceptsFalse): void {}
public function acceptsNull(null $acceptsNull): void {}
}

/**
* @see https://php.watch/versions/8.2/constants-in-traits
*/
trait TraitWithPhp82Constant
{
protected const CURRENT_VERSION = '2.6';
final protected const MIN_VERSION = '2.5';

protected function ensureVersion(): void
{
if (\Go\ParserReflection\Stub\ClassWithPhp82NullFalseTypes::CURRENT_VERSION < \Go\ParserReflection\Stub\ClassWithPhp82NullFalseTypes::MIN_VERSION) {
throw new \Exception('Current version is too old');
}
}
}

class ClassWithPhp82SensitiveAttribute
{
private string $secret;

public function __construct(#[\SensitiveParameter] string $secret = 'password')
{
$this->secret = $secret;
}
}

0 comments on commit 9f33694

Please sign in to comment.