diff --git a/src/Tokenizer/CT.php b/src/Tokenizer/CT.php index a9815fe0e0b..eefe46d3fe4 100644 --- a/src/Tokenizer/CT.php +++ b/src/Tokenizer/CT.php @@ -58,6 +58,8 @@ final class CT public const T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE = 10_037; public const T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN = 10_038; public const T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE = 10_039; + public const T_PROPERTY_HOOK_BRACE_OPEN = 10_040; + public const T_PROPERTY_HOOK_BRACE_CLOSE = 10_041; private function __construct() {} diff --git a/src/Tokenizer/Transformer/BraceTransformer.php b/src/Tokenizer/Transformer/BraceTransformer.php index 2ab09f8c633..269e208178d 100644 --- a/src/Tokenizer/Transformer/BraceTransformer.php +++ b/src/Tokenizer/Transformer/BraceTransformer.php @@ -28,7 +28,8 @@ * - in `$foo->{$bar}` into CT::T_DYNAMIC_PROP_BRACE_OPEN and CT::T_DYNAMIC_PROP_BRACE_CLOSE, * - in `${$foo}` into CT::T_DYNAMIC_VAR_BRACE_OPEN and CT::T_DYNAMIC_VAR_BRACE_CLOSE, * - in `$array{$index}` into CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN and CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, - * - in `use some\a\{ClassA, ClassB, ClassC as C}` into CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_GROUP_IMPORT_BRACE_CLOSE. + * - in `use some\a\{ClassA, ClassB, ClassC as C}` into CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_GROUP_IMPORT_BRACE_CLOSE, + * - in `class PropertyHooks { public string $bar _{_ set(string $value) { } _}_` into CT::T_PROPERTY_HOOK_BRACE_OPEN, CT::T_PROPERTY_HOOK_BRACE_CLOSE. * * @author Dariusz RumiƄski * @@ -43,13 +44,14 @@ public function getRequiredPhpVersionId(): int public function process(Tokens $tokens, Token $token, int $index): void { - $this->transformIntoCurlyCloseBrace($tokens, $token, $index); - $this->transformIntoDollarCloseBrace($tokens, $token, $index); - $this->transformIntoDynamicPropBraces($tokens, $token, $index); - $this->transformIntoDynamicVarBraces($tokens, $token, $index); - $this->transformIntoCurlyIndexBraces($tokens, $token, $index); - $this->transformIntoGroupUseBraces($tokens, $token, $index); - $this->transformIntoDynamicClassConstantFetchBraces($tokens, $token, $index); + $this->transformIntoCurlyCloseBrace($tokens, $index); + $this->transformIntoDollarCloseBrace($tokens, $index); + $this->transformIntoDynamicPropBraces($tokens, $index); + $this->transformIntoDynamicVarBraces($tokens, $index); + $this->transformIntoPropertyHookBraces($tokens, $index); + $this->transformIntoCurlyIndexBraces($tokens, $index); + $this->transformIntoGroupUseBraces($tokens, $index); + $this->transformIntoDynamicClassConstantFetchBraces($tokens, $index); } public function getCustomTokens(): array @@ -67,6 +69,8 @@ public function getCustomTokens(): array CT::T_GROUP_IMPORT_BRACE_CLOSE, CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN, CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE, + CT::T_PROPERTY_HOOK_BRACE_OPEN, + CT::T_PROPERTY_HOOK_BRACE_CLOSE, ]; } @@ -75,8 +79,10 @@ public function getCustomTokens(): array * * This should be done at very beginning of curly braces transformations. */ - private function transformIntoCurlyCloseBrace(Tokens $tokens, Token $token, int $index): void + private function transformIntoCurlyCloseBrace(Tokens $tokens, int $index): void { + $token = $tokens[$index]; + if (!$token->isGivenKind(T_CURLY_OPEN)) { return; } @@ -96,16 +102,20 @@ private function transformIntoCurlyCloseBrace(Tokens $tokens, Token $token, int $tokens[$index] = new Token([CT::T_CURLY_CLOSE, '}']); } - private function transformIntoDollarCloseBrace(Tokens $tokens, Token $token, int $index): void + private function transformIntoDollarCloseBrace(Tokens $tokens, int $index): void { + $token = $tokens[$index]; + if ($token->isGivenKind(T_DOLLAR_OPEN_CURLY_BRACES)) { $nextIndex = $tokens->getNextTokenOfKind($index, ['}']); $tokens[$nextIndex] = new Token([CT::T_DOLLAR_CLOSE_CURLY_BRACES, '}']); } } - private function transformIntoDynamicPropBraces(Tokens $tokens, Token $token, int $index): void + private function transformIntoDynamicPropBraces(Tokens $tokens, int $index): void { + $token = $tokens[$index]; + if (!$token->isObjectOperator()) { return; } @@ -121,8 +131,10 @@ private function transformIntoDynamicPropBraces(Tokens $tokens, Token $token, in $tokens[$closeIndex] = new Token([CT::T_DYNAMIC_PROP_BRACE_CLOSE, '}']); } - private function transformIntoDynamicVarBraces(Tokens $tokens, Token $token, int $index): void + private function transformIntoDynamicVarBraces(Tokens $tokens, int $index): void { + $token = $tokens[$index]; + if (!$token->equals('$')) { return; } @@ -145,13 +157,51 @@ private function transformIntoDynamicVarBraces(Tokens $tokens, Token $token, int $tokens[$closeIndex] = new Token([CT::T_DYNAMIC_VAR_BRACE_CLOSE, '}']); } - private function transformIntoCurlyIndexBraces(Tokens $tokens, Token $token, int $index): void + private function transformIntoPropertyHookBraces(Tokens $tokens, int $index): void + { + if (\PHP_VERSION_ID < 8_04_00) { + return; // @TODO: drop condition when PHP 8.4+ is required or majority of the users are using 8.4+ + } + + $token = $tokens[$index]; + + if (!$token->equals('{')) { + return; + } + + $nextIndex = $tokens->getNextMeaningfulToken($index); + + // @TODO: drop condition when PHP 8.0+ is required + if (\defined('T_ATTRIBUTE')) { + // skip attributes + while ($tokens[$nextIndex]->isGivenKind(T_ATTRIBUTE)) { + $nextIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $nextIndex); + $nextIndex = $tokens->getNextMeaningfulToken($nextIndex); + } + } + + if (!$tokens[$nextIndex]->equalsAny([ + [T_STRING, 'get'], + [T_STRING, 'set'], + ])) { + return; + } + + $closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $index); + + $tokens[$index] = new Token([CT::T_PROPERTY_HOOK_BRACE_OPEN, '{']); + $tokens[$closeIndex] = new Token([CT::T_PROPERTY_HOOK_BRACE_CLOSE, '}']); + } + + private function transformIntoCurlyIndexBraces(Tokens $tokens, int $index): void { // as $arr{index} is illegal since 8.0 if (\PHP_VERSION_ID >= 8_00_00) { return; } + $token = $tokens[$index]; + if (!$token->equals('{')) { return; } @@ -190,8 +240,10 @@ private function transformIntoCurlyIndexBraces(Tokens $tokens, Token $token, int $tokens[$closeIndex] = new Token([CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, '}']); } - private function transformIntoGroupUseBraces(Tokens $tokens, Token $token, int $index): void + private function transformIntoGroupUseBraces(Tokens $tokens, int $index): void { + $token = $tokens[$index]; + if (!$token->equals('{')) { return; } @@ -208,12 +260,14 @@ private function transformIntoGroupUseBraces(Tokens $tokens, Token $token, int $ $tokens[$closeIndex] = new Token([CT::T_GROUP_IMPORT_BRACE_CLOSE, '}']); } - private function transformIntoDynamicClassConstantFetchBraces(Tokens $tokens, Token $token, int $index): void + private function transformIntoDynamicClassConstantFetchBraces(Tokens $tokens, int $index): void { if (\PHP_VERSION_ID < 8_03_00) { return; // @TODO: drop condition when PHP 8.3+ is required or majority of the users are using 8.3+ } + $token = $tokens[$index]; + if (!$token->equals('{')) { return; } diff --git a/tests/Tokenizer/Transformer/BraceTransformerTest.php b/tests/Tokenizer/Transformer/BraceTransformerTest.php index 35b405ba5ca..a9914755d29 100644 --- a/tests/Tokenizer/Transformer/BraceTransformerTest.php +++ b/tests/Tokenizer/Transformer/BraceTransformerTest.php @@ -52,6 +52,8 @@ public function testProcess(string $source, array $expectedTokens = []): void CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_GROUP_IMPORT_BRACE_CLOSE, + CT::T_PROPERTY_HOOK_BRACE_OPEN, + CT::T_PROPERTY_HOOK_BRACE_CLOSE, ] ); } @@ -185,6 +187,8 @@ public function testProcess80(string $source, array $expectedTokens = []): void CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_GROUP_IMPORT_BRACE_CLOSE, + CT::T_PROPERTY_HOOK_BRACE_OPEN, + CT::T_PROPERTY_HOOK_BRACE_CLOSE, ] ); } @@ -225,6 +229,8 @@ public function testPre84Process(string $source, array $expectedTokens = []): vo CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_GROUP_IMPORT_BRACE_CLOSE, + CT::T_PROPERTY_HOOK_BRACE_OPEN, + CT::T_PROPERTY_HOOK_BRACE_CLOSE, ] ); } @@ -296,6 +302,201 @@ public static function providePre84ProcessCases(): iterable ]; } + /** + * @param _TransformerTestExpectedTokens $expectedTokens + * + * @dataProvider provideStarting84ProcessCases + * + * @requires PHP 8.4 + */ + public function testStarting84Process(string $source, array $expectedTokens = []): void + { + $this->doTest( + $source, + $expectedTokens, + [ + T_CURLY_OPEN, + CT::T_CURLY_CLOSE, + T_DOLLAR_OPEN_CURLY_BRACES, + CT::T_DOLLAR_CLOSE_CURLY_BRACES, + CT::T_DYNAMIC_PROP_BRACE_OPEN, + CT::T_DYNAMIC_PROP_BRACE_CLOSE, + CT::T_DYNAMIC_VAR_BRACE_OPEN, + CT::T_DYNAMIC_VAR_BRACE_CLOSE, + CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN, + CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, + CT::T_GROUP_IMPORT_BRACE_OPEN, + CT::T_GROUP_IMPORT_BRACE_CLOSE, + CT::T_PROPERTY_HOOK_BRACE_OPEN, + CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ] + ); + } + + /** + * @return iterable}> + */ + public static function provideStarting84ProcessCases(): iterable + { + yield 'property hooks: property without default value' => [ + <<<'PHP' + bar = strtolower($value); + } + } // << this one + } + PHP, + [ + 13 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 40 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: property with default value (string)' => [ + <<<'PHP' + bar = strtolower($value); + } + } // << this one + } + PHP, + [ + 17 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 44 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: property with default value (array)' => [ + <<<'PHP' + bar = $value; + } + } // << this one + } + PHP, + [ + 21 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 43 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: property with default value (namespaced)' => [ + <<<'PHP' + bar = $value; + } + } // << this one + } + PHP, + [ + 17 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 39 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: property with setter attributes' => [ + <<<'PHP' + bar = strtolower($value); + } + } // << this one + } + PHP, + [ + 13 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 48 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: property with short setter' => [ + <<<'PHP' + bar = strtolower($value); + } + } // << this one + } + PHP, + [ + 13 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 35 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: property with short getter' => [ + <<<'PHP' + ucwords(mb_strtolower($this->bar)); + } // << this one + } + PHP, + [ + 13 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 32 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + + yield 'property hooks: some more curly braces within hook' => [ + <<<'PHP' + callable = $value; + } else { + $this->callable = static function (): void { + $foo = new class implements \Stringable { + public function __toString(): string { + echo 'Na'; + } + }; + + for ($i = 0; $i < 8; $i++) { + echo (string) $foo; + } + }; + } + } + } // << this one + } + PHP, + [ + 11 => CT::T_PROPERTY_HOOK_BRACE_OPEN, + 143 => CT::T_PROPERTY_HOOK_BRACE_CLOSE, + ], + ]; + } + /** * @dataProvider provideNotDynamicClassConstantFetchCases */