Skip to content

Commit e4b852a

Browse files
committed
Support property access hooks
1 parent cc986cd commit e4b852a

File tree

7 files changed

+258
-4
lines changed

7 files changed

+258
-4
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
/*---------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
namespace Microsoft\PhpParser\Node\DelimitedList;
8+
9+
use Microsoft\PhpParser\Node\DelimitedList;
10+
11+
class PropertyElementList extends DelimitedList {
12+
}

src/Node/PropertyDeclaration.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Microsoft\PhpParser\ModifiedTypeInterface;
1111
use Microsoft\PhpParser\ModifiedTypeTrait;
1212
use Microsoft\PhpParser\Node;
13+
use Microsoft\PhpParser\Node\DelimitedList\PropertyElementList;
1314
use Microsoft\PhpParser\Node\DelimitedList\QualifiedNameList;
1415
use Microsoft\PhpParser\Token;
1516

@@ -25,7 +26,7 @@ class PropertyDeclaration extends Node implements ModifiedTypeInterface {
2526
/** @var QualifiedNameList|MissingToken|null */
2627
public $typeDeclarationList;
2728

28-
/** @var DelimitedList\ExpressionList */
29+
/** @var PropertyElementList */
2930
public $propertyElements;
3031

3132
/** @var Token */

src/Node/PropertyElement.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
/*---------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
namespace Microsoft\PhpParser\Node;
8+
9+
use Microsoft\PhpParser\Node;
10+
use Microsoft\PhpParser\Node\Expression\Variable;
11+
use Microsoft\PhpParser\Token;
12+
13+
class PropertyElement extends Node {
14+
/** @var Variable|null */
15+
public $variable;
16+
17+
/** @var Token|null */
18+
public $equalsToken;
19+
20+
/** @var Node|null */
21+
public $initializer;
22+
23+
/** @var PropertyHookList|null */
24+
public $hookList;
25+
26+
const CHILD_NAMES = [
27+
'variable',
28+
'equalsToken',
29+
'initializer',
30+
'hookList',
31+
];
32+
}

src/Node/PropertyHook.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/*---------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
namespace Microsoft\PhpParser\Node;
8+
9+
use Microsoft\PhpParser\Node;
10+
use Microsoft\PhpParser\Node\DelimitedList\ParameterDeclarationList;
11+
use Microsoft\PhpParser\Node\Statement\CompoundStatementNode;
12+
use Microsoft\PhpParser\Token;
13+
14+
class PropertyHook extends Node {
15+
/** @var AttributeGroup[]|null */
16+
public $attributes;
17+
18+
/** @var Token|null */
19+
public $hookKeyword;
20+
21+
/** @var Token|null */
22+
public $openParen;
23+
24+
/** @var ParameterDeclarationList|null */
25+
public $parameterList;
26+
27+
/** @var Token|null */
28+
public $closeParen;
29+
30+
/** @var Token|null */
31+
public $arrowToken;
32+
33+
/** @var Node|null */
34+
public $expression;
35+
36+
/** @var CompoundStatementNode|null */
37+
public $compoundStatement;
38+
39+
/** @var Token|null */
40+
public $semicolon;
41+
42+
const CHILD_NAMES = [
43+
'attributes',
44+
'hookKeyword',
45+
'openParen',
46+
'parameterList',
47+
'closeParen',
48+
'arrowToken',
49+
'expression',
50+
'compoundStatement',
51+
'semicolon',
52+
];
53+
}

src/Node/PropertyHookList.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/*---------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
namespace Microsoft\PhpParser\Node;
8+
9+
use Microsoft\PhpParser\Node;
10+
use Microsoft\PhpParser\Token;
11+
12+
class PropertyHookList extends Node {
13+
/** @var Token|null */
14+
public $openBrace;
15+
16+
/** @var PropertyHook[]|null */
17+
public $hooks;
18+
19+
/** @var Token|null */
20+
public $closeBrace;
21+
22+
const CHILD_NAMES = [
23+
'openBrace',
24+
'hooks',
25+
'closeBrace',
26+
];
27+
}

src/Parser.php

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@
7373
use Microsoft\PhpParser\Node\NamespaceUseGroupClause;
7474
use Microsoft\PhpParser\Node\NumericLiteral;
7575
use Microsoft\PhpParser\Node\ParenthesizedIntersectionType;
76+
use Microsoft\PhpParser\MissingToken;
7677
use Microsoft\PhpParser\Node\PropertyDeclaration;
78+
use Microsoft\PhpParser\Node\PropertyElement;
79+
use Microsoft\PhpParser\Node\PropertyHook;
80+
use Microsoft\PhpParser\Node\PropertyHookList;
7781
use Microsoft\PhpParser\Node\ReservedWord;
7882
use Microsoft\PhpParser\Node\StringLiteral;
7983
use Microsoft\PhpParser\Node\MethodDeclaration;
@@ -3423,12 +3427,135 @@ private function parsePropertyDeclaration($parentNode, $modifiers, $questionToke
34233427
} elseif ($questionToken) {
34243428
$propertyDeclaration->typeDeclarationList = new MissingToken(TokenKind::PropertyType, $this->token->fullStart);
34253429
}
3426-
$propertyDeclaration->propertyElements = $this->parseExpressionList($propertyDeclaration);
3427-
$propertyDeclaration->semicolon = $this->eat1(TokenKind::SemicolonToken);
3430+
$propertyDeclaration->propertyElements = $this->parsePropertyElementList($propertyDeclaration);
3431+
$requiresSemicolon = true;
3432+
foreach ($propertyDeclaration->propertyElements->children ?? [] as $child) {
3433+
if ($child instanceof PropertyElement && $child->hookList !== null) {
3434+
$requiresSemicolon = false;
3435+
break;
3436+
}
3437+
}
3438+
if ($requiresSemicolon) {
3439+
$propertyDeclaration->semicolon = $this->eat1(TokenKind::SemicolonToken);
3440+
} else {
3441+
$propertyDeclaration->semicolon = $this->eatOptional1(TokenKind::SemicolonToken);
3442+
}
34283443

34293444
return $propertyDeclaration;
34303445
}
34313446

3447+
private function parsePropertyElementList($parentNode): DelimitedList\PropertyElementList
3448+
{
3449+
return $this->parseDelimitedList(
3450+
DelimitedList\PropertyElementList::class,
3451+
TokenKind::CommaToken,
3452+
$this->isPropertyElementStartFn(),
3453+
function ($list) {
3454+
return $this->parsePropertyElement($list);
3455+
},
3456+
$parentNode
3457+
) ?? new DelimitedList\PropertyElementList();
3458+
}
3459+
3460+
private function isPropertyElementStartFn()
3461+
{
3462+
return function (Token $token): bool {
3463+
return $token->kind === TokenKind::VariableName || $token->kind === TokenKind::DollarToken;
3464+
};
3465+
}
3466+
3467+
private function parsePropertyElement($parentNode): PropertyElement
3468+
{
3469+
$element = new PropertyElement();
3470+
$element->parent = $parentNode;
3471+
$element->variable = $this->parseSimpleVariable($element);
3472+
$element->equalsToken = $this->eatOptional1(TokenKind::EqualsToken);
3473+
if ($element->equalsToken !== null) {
3474+
$element->initializer = $this->parseExpression($element);
3475+
}
3476+
if ($this->getCurrentToken()->kind === TokenKind::OpenBraceToken) {
3477+
$element->hookList = $this->parsePropertyHookList($element);
3478+
}
3479+
return $element;
3480+
}
3481+
3482+
private function parsePropertyHookList(PropertyElement $parentNode): PropertyHookList
3483+
{
3484+
$hookList = new PropertyHookList();
3485+
$hookList->parent = $parentNode;
3486+
$hookList->openBrace = $this->eat1(TokenKind::OpenBraceToken);
3487+
$hookList->hooks = [];
3488+
3489+
while (true) {
3490+
$token = $this->getCurrentToken();
3491+
if ($token->kind === TokenKind::CloseBraceToken || $token->kind === TokenKind::EndOfFileToken) {
3492+
break;
3493+
}
3494+
$previousFullStart = $token->fullStart;
3495+
$hook = $this->parsePropertyHook($hookList);
3496+
if ($hook instanceof PropertyHook) {
3497+
$hookList->hooks[] = $hook;
3498+
} else {
3499+
// Ensure forward progress to avoid infinite loops.
3500+
$this->advanceToken();
3501+
}
3502+
if ($this->getCurrentToken()->fullStart === $previousFullStart) {
3503+
// No progress was made; advance once to prevent infinite loops.
3504+
$this->advanceToken();
3505+
}
3506+
}
3507+
3508+
$hookList->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
3509+
return $hookList;
3510+
}
3511+
3512+
private function parsePropertyHook(PropertyHookList $parentNode): ?PropertyHook
3513+
{
3514+
$hook = new PropertyHook();
3515+
$hook->parent = $parentNode;
3516+
3517+
if ($this->getCurrentToken()->kind === TokenKind::AttributeToken) {
3518+
$hook->attributes = $this->parseAttributeGroups($hook);
3519+
}
3520+
3521+
$token = $this->getCurrentToken();
3522+
if ($token->kind === TokenKind::Name) {
3523+
$text = \strtolower(\trim($token->getText($this->sourceFile->fileContents)));
3524+
if (\in_array($text, ['get', 'set', 'init'], true)) {
3525+
$hook->hookKeyword = $token;
3526+
$this->advanceToken();
3527+
} else {
3528+
$hook->hookKeyword = new MissingToken(TokenKind::Name, $token->fullStart);
3529+
return $hook;
3530+
}
3531+
} else {
3532+
$hook->hookKeyword = new MissingToken(TokenKind::Name, $token->fullStart);
3533+
return $hook;
3534+
}
3535+
3536+
if ($this->checkToken(TokenKind::OpenParenToken)) {
3537+
$hook->openParen = $this->eat1(TokenKind::OpenParenToken);
3538+
$hook->parameterList = $this->parseDelimitedList(
3539+
DelimitedList\ParameterDeclarationList::class,
3540+
TokenKind::CommaToken,
3541+
$this->isParameterStartFn(),
3542+
$this->parseParameterFn(),
3543+
$hook
3544+
);
3545+
$hook->closeParen = $this->eat1(TokenKind::CloseParenToken);
3546+
}
3547+
3548+
if ($this->checkToken(TokenKind::DoubleArrowToken)) {
3549+
$hook->arrowToken = $this->eat1(TokenKind::DoubleArrowToken);
3550+
$hook->expression = $this->parseExpression($hook);
3551+
$hook->semicolon = $this->eatOptional1(TokenKind::SemicolonToken) ?? new MissingToken(TokenKind::SemicolonToken, $this->getCurrentToken()->fullStart);
3552+
} else {
3553+
$hook->compoundStatement = $this->parseCompoundStatement($hook);
3554+
}
3555+
3556+
return $hook;
3557+
}
3558+
34323559
/**
34333560
* Parse a comma separated qualified name list (e.g. interfaces implemented by a class)
34343561
*

tests/samples/property_hooks.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ class Counter {
55

66
public int $count {
77
get => $this->value;
8-
set (int $newValue) => $this->value = max(0, $newValue);
8+
set (int $newValue) {
9+
$this->value = max(0, $newValue);
10+
}
911
}
1012
}
1113

0 commit comments

Comments
 (0)