Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor #504

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading