Skip to content

Commit e20f450

Browse files
authored
Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor (#504)
Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor
1 parent 452a023 commit e20f450

File tree

7 files changed

+426
-6
lines changed

7 files changed

+426
-6
lines changed

Diff for: src/Instrument/Transformer/SelfValueVisitor.php

+24-6
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
use PhpParser\Node\Expr\New_;
2020
use PhpParser\Node\Expr\StaticCall;
2121
use PhpParser\Node\Identifier;
22+
use PhpParser\Node\IntersectionType;
2223
use PhpParser\Node\Name;
2324
use PhpParser\Node\Name\FullyQualified;
2425
use PhpParser\Node\NullableType;
2526
use PhpParser\Node\Param;
2627
use PhpParser\Node\Stmt\Catch_;
27-
use PhpParser\Node\Stmt\Class_;
28+
use PhpParser\Node\Stmt\ClassLike;
2829
use PhpParser\Node\Stmt\ClassMethod;
2930
use PhpParser\Node\Stmt\Namespace_;
3031
use PhpParser\Node\Stmt\Property;
32+
use PhpParser\Node\Stmt\Trait_;
33+
use PhpParser\Node\UnionType;
3134
use PhpParser\NodeVisitorAbstract;
3235
use UnexpectedValueException;
3336

@@ -82,10 +85,6 @@ public function enterNode(Node $node)
8285
{
8386
if ($node instanceof Namespace_) {
8487
$this->namespace = !empty($node->name) ? $node->name->toString() : null;
85-
} elseif ($node instanceof Class_) {
86-
if ($node->name !== null) {
87-
$this->className = new Name($node->name->toString());
88-
}
8988
} elseif ($node instanceof ClassMethod || $node instanceof Closure) {
9089
if (isset($node->returnType)) {
9190
$node->returnType = $this->resolveType($node->returnType);
@@ -107,6 +106,12 @@ public function enterNode(Node $node)
107106
foreach ($node->types as &$type) {
108107
$type = $this->resolveClassName($type);
109108
}
109+
} elseif ($node instanceof ClassLike) {
110+
if (! $node instanceof Trait_) {
111+
$this->className = !empty($node->name) ? new Name($node->name->toString()) : null;
112+
} else {
113+
$this->className = null;
114+
}
110115
}
111116

112117
return null;
@@ -126,6 +131,10 @@ protected function resolveClassName(Name $name): Name
126131
return $name;
127132
}
128133

134+
if ($this->className === null) {
135+
return $name;
136+
}
137+
129138
// Save the original name
130139
$originalName = $name;
131140
$name = clone $originalName;
@@ -142,7 +151,7 @@ protected function resolveClassName(Name $name): Name
142151
/**
143152
* Helper method for resolving type nodes
144153
*
145-
* @return NullableType|Name|FullyQualified|Identifier
154+
* @return NullableType|Name|FullyQualified|Identifier|UnionType|IntersectionType
146155
*/
147156
private function resolveType(Node $node)
148157
{
@@ -157,6 +166,15 @@ private function resolveType(Node $node)
157166
return $node;
158167
}
159168

169+
if ($node instanceof UnionType || $node instanceof IntersectionType) {
170+
$types = [];
171+
foreach ($node->types as $type) {
172+
$types[] = $this->resolveType($type);
173+
}
174+
$node->types = $types;
175+
return $node;
176+
}
177+
160178
throw new UnexpectedValueException('Unknown node type: ' . get_class($node));
161179
}
162180
}

Diff for: tests/Go/Instrument/Transformer/SelfValueTransformerTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,9 @@ public static function filesDataProvider(): \Generator
9999
__DIR__ . '/_files/php82-file.php',
100100
__DIR__ . '/_files/php82-file-transformed.php'
101101
];
102+
yield 'anonymous-class.php' => [
103+
__DIR__ . '/_files/anonymous-class.php',
104+
__DIR__ . '/_files/anonymous-class-transformed.php'
105+
];
102106
}
103107
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/**
3+
* Parser Reflection API
4+
*
5+
* @copyright Copyright 2016, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace Go\ParserReflection\Stub;
13+
14+
class InAnonymousClass
15+
{
16+
public function respond()
17+
{
18+
new class {
19+
public const FOO = 'foo';
20+
21+
public function run()
22+
{
23+
return self::FOO;
24+
}
25+
};
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/**
3+
* Parser Reflection API
4+
*
5+
* @copyright Copyright 2016, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace Go\ParserReflection\Stub;
13+
14+
class InAnonymousClass
15+
{
16+
public function respond()
17+
{
18+
new class {
19+
public const FOO = 'foo';
20+
21+
public function run()
22+
{
23+
return self::FOO;
24+
}
25+
};
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
/**
3+
* Parser Reflection API
4+
*
5+
* @copyright Copyright 2016, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace Go\ParserReflection\Stub;
13+
14+
use Attribute;
15+
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};
16+
17+
class ClassWithPhp80Features
18+
{
19+
public function acceptsStringArrayDefaultToNull(array|string $iterable = null) : array {}
20+
}
21+
22+
/**
23+
* @see https://php.watch/versions/8.0/named-parameters
24+
*/
25+
class ClassWithPHP80NamedCall
26+
{
27+
public static function foo(string $key1 = '', string $key2 = ''): string
28+
{
29+
return $key1 . ':' . $key2;
30+
}
31+
32+
public static function namedCall(): array
33+
{
34+
return [
35+
'key1' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key1: 'bar'),
36+
'key2' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key2: 'baz'),
37+
'keys' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key1: 'A', key2: 'B'),
38+
'reverseKeys' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key2: 'A', key1: 'B'),
39+
'unpack' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(...['key1' => 'C', 'key2' => 'D']),
40+
];
41+
}
42+
}
43+
44+
/**
45+
* @see https://php.watch/versions/8.0/attributes
46+
*/
47+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
48+
readonly class ClassPHP80Attribute
49+
{
50+
private string $value;
51+
52+
public function __construct(string $value)
53+
{
54+
$this->value = $value;
55+
}
56+
57+
public function getValue(): string
58+
{
59+
return $this->value;
60+
}
61+
}
62+
63+
/**
64+
* @see https://php.watch/versions/8.0/attributes
65+
*/
66+
#[ClassPHP80Attribute('class')]
67+
class ClassPHP80WithAttribute
68+
{
69+
#[ClassPHP80Attribute('first')]
70+
#[ClassPHP80Attribute('second')]
71+
public const PUBLIC_CONST = 1;
72+
73+
#[ClassPHP80Attribute('property')]
74+
private string $privateProperty = 'foo';
75+
76+
#[ClassPHP80Attribute('method')]
77+
public function bar(#[ClassPHP80Attribute('parameter')] $parameter)
78+
{}
79+
}
80+
81+
/**
82+
* @see https://php.watch/versions/8.0/constructor-property-promotion
83+
*/
84+
class ClassPHP80WithPropertyPromotion
85+
{
86+
public function __construct(
87+
private string $privateStringValue,
88+
private $privateNonTypedValue,
89+
protected int $protectedIntValue = 42,
90+
public array $publicArrayValue = [M_PI, M_E],
91+
) {}
92+
}
93+
94+
/**
95+
* @see https://php.watch/versions/8.0/union-types
96+
*/
97+
class ClassWithPHP80UnionTypes
98+
{
99+
public string|int|float|bool $scalarValue;
100+
101+
public array|object|null $complexValueOrNull = null;
102+
103+
/**
104+
* Special case, internally iterable should be replaced with Traversable|array
105+
*/
106+
public iterable|object $iterableOrObject;
107+
108+
public static function returnsUnionType(): object|array|null {}
109+
110+
public static function acceptsUnionType(\stdClass|\Traversable|array $iterable): void {}
111+
}
112+
113+
/**
114+
* @see https://php.watch/versions/8.0/mixed-type
115+
*/
116+
class ClassWithPHP80MixedType
117+
{
118+
public mixed $someMixedPublicProperty;
119+
120+
public static function returnsMixed(): mixed {}
121+
122+
public static function acceptsMixed(mixed $value): void {}
123+
}
124+
125+
/**
126+
* @see https://php.watch/versions/8.0/static-return-type
127+
*/
128+
class ClassWithPHP80StaticReturnType
129+
{
130+
public static function create(): static {}
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
/**
3+
* Parser Reflection API
4+
*
5+
* @copyright Copyright 2024, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace Go\ParserReflection\Stub;
13+
14+
/**
15+
* @see https://php.watch/versions/8.1/readonly
16+
*/
17+
class ClassWithPhp81ReadOnlyProperties
18+
{
19+
public readonly int $publicReadonlyInt;
20+
21+
protected readonly array $protectedReadonlyArray;
22+
23+
private readonly object $privateReadonlyObject;
24+
}
25+
26+
/**
27+
* @see https://php.watch/versions/8.1/enums
28+
*/
29+
enum SimplePhp81EnumWithSuit {
30+
case Clubs;
31+
case Diamonds;
32+
case Hearts;
33+
case Spades;
34+
}
35+
36+
/**
37+
* @see https://php.watch/versions/8.1/enums#enums-backed
38+
*/
39+
enum BackedPhp81EnumHTTPMethods: string
40+
{
41+
case GET = 'get';
42+
case POST = 'post';
43+
}
44+
45+
/**
46+
* @see https://php.watch/versions/8.1/enums#enum-methods
47+
*/
48+
enum BackedPhp81EnumHTTPStatusWithMethod: int
49+
{
50+
case OK = 200;
51+
case ACCESS_DENIED = 403;
52+
case NOT_FOUND = 404;
53+
54+
public function label(): string {
55+
return static::getLabel($this);
56+
}
57+
58+
public static function getLabel(\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod $value): string {
59+
return match ($value) {
60+
\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::OK => 'OK',
61+
\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::ACCESS_DENIED => 'Access Denied',
62+
\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::NOT_FOUND => 'Page Not Found',
63+
};
64+
}
65+
}
66+
67+
/**
68+
* @see https://php.watch/versions/8.1/intersection-types
69+
*/
70+
class ClassWithPhp81IntersectionType implements \Countable
71+
{
72+
private \Iterator&\Countable $countableIterator;
73+
74+
public function __construct(\Iterator&\Countable $countableIterator)
75+
{
76+
$this->countableIterator = $countableIterator;
77+
}
78+
79+
public function count(): int
80+
{
81+
return count($this->countableIterator);
82+
}
83+
}
84+
85+
/**
86+
* @see https://php.watch/versions/8.1/intersection-types
87+
*/
88+
function functionWithPhp81IntersectionType(\Iterator&\Countable $value): \Iterator&\Countable {
89+
foreach($value as $val) {}
90+
count($value);
91+
92+
return $value;
93+
}
94+
95+
/**
96+
* @see https://php.watch/versions/8.1/never-return-type
97+
*/
98+
class ClassWithPhp81NeverReturnType
99+
{
100+
public static function doThis(): never
101+
{
102+
throw new \RuntimeException('Not implemented');
103+
}
104+
}
105+
106+
/**
107+
* @see https://php.watch/versions/8.1/never-return-type
108+
*/
109+
function functionWithPhp81NeverReturnType(): never
110+
{
111+
throw new \RuntimeException('Not implemented');
112+
}
113+
114+
/**
115+
* @see https://php.watch/versions/8.1/final-class-const
116+
*/
117+
class ClassWithPhp81FinalClassConst {
118+
final public const TEST = '1';
119+
}

0 commit comments

Comments
 (0)