Skip to content

Commit d5ac20e

Browse files
committed
feat: add support for property access in conditional types
1 parent f1ddda6 commit d5ac20e

File tree

7 files changed

+588
-12
lines changed

7 files changed

+588
-12
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use function sprintf;
7+
8+
class ConditionalTypeForPropertyNode implements TypeNode
9+
{
10+
11+
use NodeAttributes;
12+
13+
public PropertyAccessNode $subject;
14+
15+
public TypeNode $targetType;
16+
17+
public TypeNode $if;
18+
19+
public TypeNode $else;
20+
21+
public bool $negated;
22+
23+
public function __construct(
24+
PropertyAccessNode $subject,
25+
TypeNode $targetType,
26+
TypeNode $if,
27+
TypeNode $else,
28+
bool $negated
29+
)
30+
{
31+
$this->subject = $subject;
32+
$this->targetType = $targetType;
33+
$this->if = $if;
34+
$this->else = $else;
35+
$this->negated = $negated;
36+
}
37+
38+
public function __toString(): string
39+
{
40+
return sprintf(
41+
'(%s %s %s ? %s : %s)',
42+
$this->subject,
43+
$this->negated ? 'is not' : 'is',
44+
$this->targetType,
45+
$this->if,
46+
$this->else,
47+
);
48+
}
49+
50+
/**
51+
* @param array<string, mixed> $properties
52+
*/
53+
public static function __set_state(array $properties): self
54+
{
55+
$instance = new self(
56+
$properties['subject'],
57+
$properties['targetType'],
58+
$properties['if'],
59+
$properties['else'],
60+
$properties['negated'],
61+
);
62+
if (isset($properties['attributes'])) {
63+
foreach ($properties['attributes'] as $key => $value) {
64+
$instance->setAttribute($key, $value);
65+
}
66+
}
67+
return $instance;
68+
}
69+
70+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\Node;
6+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
7+
use function array_map;
8+
use function implode;
9+
10+
/**
11+
* Represents a property access expression in conditional types.
12+
*/
13+
class PropertyAccessNode implements Node
14+
{
15+
16+
use NodeAttributes;
17+
18+
public const HOLDER_SELF = 'self';
19+
public const HOLDER_PARENT = 'parent';
20+
public const HOLDER_STATIC = 'static';
21+
22+
public bool $isStatic;
23+
24+
/**
25+
* For static access: 'self', 'parent', or 'static'
26+
* For instance access: null (holder is implicitly $this)
27+
*
28+
* @var self::HOLDER_*|null
29+
*/
30+
public ?string $holder;
31+
32+
/** @var list<PropertyAccessPathItem> */
33+
public array $path;
34+
35+
/**
36+
* @param self::HOLDER_*|null $holder
37+
* @param list<PropertyAccessPathItem> $path
38+
*/
39+
public function __construct(bool $isStatic, ?string $holder, array $path)
40+
{
41+
$this->isStatic = $isStatic;
42+
$this->holder = $holder;
43+
$this->path = $path;
44+
}
45+
46+
public function __toString(): string
47+
{
48+
if ($this->isStatic) {
49+
return $this->holder . '::$' . $this->path[0]->name;
50+
}
51+
52+
$pathString = implode('->', array_map(
53+
static fn (PropertyAccessPathItem $item): string => $item->name,
54+
$this->path,
55+
));
56+
57+
return '$this->' . $pathString;
58+
}
59+
60+
/**
61+
* @param array<string, mixed> $properties
62+
*/
63+
public static function __set_state(array $properties): self
64+
{
65+
$instance = new self(
66+
$properties['isStatic'],
67+
$properties['holder'],
68+
$properties['path'],
69+
);
70+
if (isset($properties['attributes'])) {
71+
foreach ($properties['attributes'] as $key => $value) {
72+
$instance->setAttribute($key, $value);
73+
}
74+
}
75+
return $instance;
76+
}
77+
78+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\Node;
6+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
7+
8+
/**
9+
* Represents a single property in a property access path.
10+
*
11+
* Examples:
12+
* - In `$this->config->database`, there are two items: 'config' and 'database'
13+
* - In `self::$cache`, there is one item: 'cache'
14+
*/
15+
class PropertyAccessPathItem implements Node
16+
{
17+
18+
use NodeAttributes;
19+
20+
public string $name;
21+
22+
public function __construct(string $name)
23+
{
24+
$this->name = $name;
25+
}
26+
27+
public function __toString(): string
28+
{
29+
return $this->name;
30+
}
31+
32+
/**
33+
* @param array<string, mixed> $properties
34+
*/
35+
public static function __set_state(array $properties): self
36+
{
37+
$instance = new self($properties['name']);
38+
if (isset($properties['attributes'])) {
39+
foreach ($properties['attributes'] as $key => $value) {
40+
$instance->setAttribute($key, $value);
41+
}
42+
}
43+
return $instance;
44+
}
45+
46+
}

src/Lexer/Lexer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ private function generateRegexp(): string
140140
$patterns = [
141141
self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++',
142142

143-
self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++',
143+
self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF](?:[0-9a-z_\\x80-\\xFF]|(?!->)-)*+)++',
144144
self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])',
145145
self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+',
146146

0 commit comments

Comments
 (0)