Browse Source

feature: PHP8.3 - Add CT and block type for `Dynamic class constant fetch` (#7004)

SpacePossum 1 year ago
parent
commit
d13be70d21

+ 2 - 0
src/Tokenizer/CT.php

@@ -56,6 +56,8 @@ final class CT
     public const T_TYPE_INTERSECTION = 10035;
     public const T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN = 10036;
     public const T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE = 10037;
+    public const T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN = 10038;
+    public const T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE = 10039;
 
     private function __construct()
     {

+ 5 - 0
src/Tokenizer/Tokens.php

@@ -47,6 +47,7 @@ class Tokens extends \SplFixedArray
     public const BLOCK_TYPE_BRACE_CLASS_INSTANTIATION = 10;
     public const BLOCK_TYPE_ATTRIBUTE = 11;
     public const BLOCK_TYPE_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS = 12;
+    public const BLOCK_TYPE_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE = 13;
 
     /**
      * Static class cache.
@@ -253,6 +254,10 @@ class Tokens extends \SplFixedArray
                 'start' => [CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, '('],
                 'end' => [CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE, ')'],
             ],
+            self::BLOCK_TYPE_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE => [
+                'start' => [CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN, '{'],
+                'end' => [CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE, '}'],
+            ],
         ];
 
         // @TODO: drop condition when PHP 8.0+ is required

+ 42 - 0
src/Tokenizer/Transformer/CurlyBraceTransformer.php

@@ -49,6 +49,7 @@ final class CurlyBraceTransformer extends AbstractTransformer
         $this->transformIntoDynamicVarBraces($tokens, $token, $index);
         $this->transformIntoCurlyIndexBraces($tokens, $token, $index);
         $this->transformIntoGroupUseBraces($tokens, $token, $index);
+        $this->transformIntoDynamicClassConstantFetchBraces($tokens, $token, $index);
     }
 
     public function getCustomTokens(): array
@@ -64,6 +65,8 @@ final class CurlyBraceTransformer extends AbstractTransformer
             CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE,
             CT::T_GROUP_IMPORT_BRACE_OPEN,
             CT::T_GROUP_IMPORT_BRACE_CLOSE,
+            CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+            CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
         ];
     }
 
@@ -200,6 +203,45 @@ final class CurlyBraceTransformer extends AbstractTransformer
         $tokens[$closeIndex] = new Token([CT::T_GROUP_IMPORT_BRACE_CLOSE, '}']);
     }
 
+    private function transformIntoDynamicClassConstantFetchBraces(Tokens $tokens, Token $token, 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+
+        }
+
+        if (!$token->equals('{')) {
+            return;
+        }
+
+        $prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($index);
+
+        while (!$tokens[$prevMeaningfulTokenIndex]->isGivenKind(T_DOUBLE_COLON)) {
+            if (!$tokens[$prevMeaningfulTokenIndex]->equals(')')) {
+                return;
+            }
+
+            $prevMeaningfulTokenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevMeaningfulTokenIndex);
+            $prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulTokenIndex);
+
+            if (!$tokens[$prevMeaningfulTokenIndex]->equals('}')) {
+                return;
+            }
+
+            $prevMeaningfulTokenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $prevMeaningfulTokenIndex);
+            $prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulTokenIndex);
+        }
+
+        $closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $index);
+        $nextMeaningfulTokenIndexAfterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
+
+        if (!$tokens[$nextMeaningfulTokenIndexAfterCloseIndex]->equalsAny([';', [T_CLOSE_TAG], [T_DOUBLE_COLON]])) {
+            return;
+        }
+
+        $tokens[$index] = new Token([CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN, '{']);
+        $tokens[$closeIndex] = new Token([CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE, '}']);
+    }
+
     /**
      * We do not want to rely on `$tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index)` here,
      * as it relies on block types that are assuming that `}` tokens are already transformed to Custom Tokens that are allowing to distinguish different block types.

+ 31 - 0
tests/Tokenizer/TokensTest.php

@@ -779,6 +779,37 @@ PHP;
         }
     }
 
+    /**
+     * @requires PHP 8.3
+     *
+     * @dataProvider provideFindBlockEnd83Cases
+     *
+     * @param Tokens::BLOCK_TYPE_* $type
+     */
+    public function testFindBlockEnd83(int $expectedIndex, string $source, int $type, int $searchIndex): void
+    {
+        self::assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex);
+    }
+
+    public static function provideFindBlockEnd83Cases(): iterable
+    {
+        yield 'simple dynamic class constant fetch' => [
+            7,
+            '<?php echo Foo::{$bar};',
+            Tokens::BLOCK_TYPE_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE,
+            5,
+        ];
+
+        foreach ([[5, 7], [9, 11]] as $startEnd) {
+            yield 'chained dynamic class constant fetch: '.$startEnd[0] => [
+                $startEnd[1],
+                "<?php echo Foo::{'BAR'}::{'BLA'}::{static_method}(1,2) ?>",
+                Tokens::BLOCK_TYPE_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE,
+                $startEnd[0],
+            ];
+        }
+    }
+
     public function testFindBlockEndInvalidType(): void
     {
         Tokens::clearCache();

+ 151 - 1
tests/Tokenizer/Transformer/CurlyBraceTransformerTest.php

@@ -16,6 +16,7 @@ namespace PhpCsFixer\Tests\Tokenizer\Transformer;
 
 use PhpCsFixer\Tests\Test\AbstractTransformerTestCase;
 use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Tokens;
 
 /**
  * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
@@ -237,7 +238,7 @@ final class CurlyBraceTransformerTest extends AbstractTransformerTestCase
     public static function provideProcess80Cases(): array
     {
         return [
-            'dynamic property brace open/close' => [
+            'dynamic nullable property brace open/close' => [
                 '<?php $foo?->{$bar};',
                 [
                     3 => CT::T_DYNAMIC_PROP_BRACE_OPEN,
@@ -246,4 +247,153 @@ final class CurlyBraceTransformerTest extends AbstractTransformerTestCase
             ],
         ];
     }
+
+    /**
+     * @dataProvider provideNotDynamicClassConstantFetchCases
+     */
+    public function testNotDynamicClassConstantFetch(string $source): void
+    {
+        Tokens::clearCache();
+        $tokens = Tokens::fromCode($source);
+
+        self::assertFalse(
+            $tokens->isAnyTokenKindsFound(
+                [
+                    CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                    CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+                ]
+            )
+        );
+    }
+
+    public static function provideNotDynamicClassConstantFetchCases(): iterable
+    {
+        yield 'negatives' => [
+            '<?php
+                namespace B {$b = Z::B;};
+
+                echo $c::{$static_method}();
+                echo Foo::{$static_method}();
+
+                echo Foo::${static_property};
+                echo Foo::${$static_property};
+
+                echo Foo::class;
+
+                echo $foo::$bar;
+                echo $foo::bar();
+                echo foo()::A();
+
+                {$z = A::C;}
+            ',
+        ];
+    }
+
+    /**
+     * @param array<int, int> $expectedTokens
+     *
+     * @dataProvider provideDynamicClassConstantFetchCases
+     *
+     * @requires PHP 8.3
+     */
+    public function testDynamicClassConstantFetch(array $expectedTokens, string $source): void
+    {
+        $this->doTest(
+            $source,
+            $expectedTokens,
+            [
+                CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+        );
+    }
+
+    public static function provideDynamicClassConstantFetchCases(): iterable
+    {
+        yield 'simple' => [
+            [
+                5 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                7 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            '<?php echo Foo::{$bar};',
+        ];
+
+        yield 'static method var, string' => [
+            [
+                10 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                12 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            "<?php echo Foo::{\$static_method}(){'XYZ'};",
+        ];
+
+        yield 'long way of writing `Bar::class`' => [
+            [
+                5 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                7 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            "<?php echo Bar::{'class'};",
+        ];
+
+        yield 'variable variable wrapped, close tag' => [
+            [
+                5 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                10 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            '<?php echo Foo::{${$var}}?>',
+        ];
+
+        yield 'variable variable, comment' => [
+            [
+                5 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                8 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            '<?php echo Foo::{$$var}/* */;?>',
+        ];
+
+        yield 'static, self' => [
+            [
+                37 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                39 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+                46 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                48 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+                55 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                57 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            '<?php
+                class Foo
+                {
+                    private const X = 1;
+
+                    public function Bar($var): void
+                    {
+                        echo self::{$var};
+                        echo static::{$var};
+                        echo static::{"X"};
+                    }
+                }
+            ',
+        ];
+
+        yield 'chained' => [
+            [
+                5 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                7 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+                9 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                11 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            "<?php echo Foo::{'BAR'}::{'BLA'}::{static_method}(1,2) ?>",
+        ];
+
+        yield 'mixed chain' => [
+            [
+                17 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                19 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+                21 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                23 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+                25 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
+                27 => CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
+            ],
+            '<?php echo Foo::{\'static_method\'}()::{$$a}(){"const"}::{some_const}::{$other_const}::{$last_static_method}();',
+        ];
+    }
 }