Просмотр исходного кода

minor: PHP8.2 - handle union and intersection types for DNF types (#6804)

Co-authored-by: Dariusz Ruminski <dariusz.ruminski@gmail.com>
Kuba Werłos 2 лет назад
Родитель
Сommit
e561bd1818

+ 26 - 55
src/Tokenizer/AbstractTypeTransformer.php

@@ -21,8 +21,19 @@ namespace PhpCsFixer\Tokenizer;
  */
 abstract class AbstractTypeTransformer extends AbstractTransformer
 {
+    private const TYPE_END_TOKENS = [')', [T_CALLABLE], [T_NS_SEPARATOR], [T_STRING], [CT::T_ARRAY_TYPEHINT]];
+
+    private const TYPE_TOKENS = [
+        '|', '&', '(',
+        ...self::TYPE_END_TOKENS,
+        [CT::T_TYPE_ALTERNATION], [CT::T_TYPE_INTERSECTION], // some siblings may already be transformed
+        [T_WHITESPACE], [T_COMMENT], [T_DOC_COMMENT], // technically these can be inside of type tokens array
+    ];
+
+    abstract protected function replaceToken(Tokens $tokens, int $index): void;
+
     /**
-     * @param array{0: int, 1?: string}|string $originalToken
+     * @param array{0: int, 1: string}|string $originalToken
      */
     protected function doProcess(Tokens $tokens, int $index, $originalToken): void
     {
@@ -30,67 +41,27 @@ abstract class AbstractTypeTransformer extends AbstractTransformer
             return;
         }
 
-        $prevIndex = $this->getPreviousTokenCandidate($tokens, $index);
-
-        /** @var Token $prevToken */
-        $prevToken = $tokens[$prevIndex];
-
-        if ($prevToken->isGivenKind([
-            CT::T_TYPE_COLON, // `:` is part of a function return type `foo(): X|Y`
-            CT::T_TYPE_ALTERNATION, // `|` is part of a union (chain) `X|Y`
-            CT::T_TYPE_INTERSECTION,
-            T_STATIC, T_VAR, T_PUBLIC, T_PROTECTED, T_PRIVATE, // `var X|Y $a;`, `private X|Y $a` or `public static X|Y $a`
-            CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, // promoted properties
-        ])) {
-            $this->replaceToken($tokens, $index);
-
-            return;
-        }
-
-        if (\defined('T_READONLY') && $prevToken->isGivenKind(T_READONLY)) { // @TODO: drop condition when PHP 8.1+ is required
-            $this->replaceToken($tokens, $index);
-
-            return;
-        }
-
-        if (!$prevToken->equalsAny(['(', ','])) {
-            return;
-        }
-
-        $prevPrevTokenIndex = $tokens->getPrevMeaningfulToken($prevIndex);
-
-        if ($tokens[$prevPrevTokenIndex]->isGivenKind(T_CATCH)) {
-            $this->replaceToken($tokens, $index);
-
-            return;
-        }
-
-        $functionKinds = [[T_FUNCTION], [T_FN]];
-        $functionIndex = $tokens->getPrevTokenOfKind($prevIndex, $functionKinds);
-
-        if (null === $functionIndex) {
-            return;
-        }
-
-        $braceOpenIndex = $tokens->getNextTokenOfKind($functionIndex, ['(']);
-        $braceCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $braceOpenIndex);
-
-        if ($braceCloseIndex < $index) {
+        if (!$this->isPartOfType($tokens, $index)) {
             return;
         }
 
         $this->replaceToken($tokens, $index);
     }
 
-    abstract protected function replaceToken(Tokens $tokens, int $index): void;
-
-    private function getPreviousTokenCandidate(Tokens $tokens, int $index): int
+    private function isPartOfType(Tokens $tokens, int $index): bool
     {
-        $candidateIndex = $tokens->getTokenNotOfKindsSibling($index, -1, [T_CALLABLE, T_NS_SEPARATOR, T_STRING, CT::T_ARRAY_TYPEHINT, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]);
+        // for parameter there will be variable after type
+        $variableIndex = $tokens->getTokenNotOfKindSibling($index, 1, self::TYPE_TOKENS);
+        if ($tokens[$variableIndex]->isGivenKind(T_VARIABLE)) {
+            return $tokens[$tokens->getPrevMeaningfulToken($variableIndex)]->equalsAny(self::TYPE_END_TOKENS);
+        }
+
+        // return types and non-capturing catches
+        $typeColonIndex = $tokens->getTokenNotOfKindSibling($index, -1, self::TYPE_TOKENS);
+        if ($tokens[$typeColonIndex]->isGivenKind([T_CATCH, CT::T_TYPE_COLON])) {
+            return true;
+        }
 
-        return $tokens[$candidateIndex]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)
-            ? $this->getPreviousTokenCandidate($tokens, $tokens->getPrevTokenOfKind($index, [[T_ATTRIBUTE]]))
-            : $candidateIndex
-        ;
+        return false;
     }
 }

+ 12 - 0
tests/Fixtures/Integration/misc/PHP8_2.test

@@ -101,6 +101,12 @@ trait WithConstants
     private const THREE = 'three';
 }
 
+// https://wiki.php.net/rfc/dnf_types
+function generateSlug((HasTitle&HasId)|null $post)
+{
+    throw new \Exception('not implemented');
+}
+
 --INPUT--
 <?php
 
@@ -191,3 +197,9 @@ trait      WithConstants      {
     protected const TWO   = 'two';
     private   const THREE = 'three';
 }
+
+// https://wiki.php.net/rfc/dnf_types
+function generateSlug((HasTitle&HasId)|null $post)
+{
+    throw new \Exception('not implemented');
+}

+ 47 - 0
tests/Tokenizer/TokensAnalyzerTest.php

@@ -1909,6 +1909,53 @@ $b;',
         ];
     }
 
+    /**
+     * @param array<int, bool> $expected
+     *
+     * @dataProvider provideIsBinaryOperator82Cases
+     *
+     * @requires PHP 8.2
+     */
+    public function testIsBinaryOperator82(array $expected, string $source): void
+    {
+        $tokens = Tokens::fromCode($source);
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+
+        foreach ($tokens as $index => $token) {
+            $isBinary = isset($expected[$index]);
+            static::assertSame($isBinary, $tokensAnalyzer->isBinaryOperator($index));
+            if ($isBinary) {
+                static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index));
+                static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index));
+            }
+        }
+    }
+
+    public static function provideIsBinaryOperator82Cases(): iterable
+    {
+        yield [
+            [],
+            '<?php class Dnf { public static I|(P&S11) $f2;}',
+        ];
+
+        yield [
+            [],
+            '<?php function Foo((A&B)|I $x): (X&Z)|(p\f\G&Y\Z)|z { return foo();}',
+        ];
+
+        $particularEndOfFile = 'A|(B&C); }';
+
+        yield sprintf('block "%s" at the end of file that is a type', $particularEndOfFile) => [
+            [],
+            '<?php abstract class A { abstract function foo(): '.$particularEndOfFile,
+        ];
+
+        yield sprintf('block "%s" at the end of file that is not a type', $particularEndOfFile) => [
+            [12 => true, 15 => true],
+            '<?php function foo() { return '.$particularEndOfFile,
+        ];
+    }
+
     /**
      * @dataProvider provideArrayExceptionsCases
      */

+ 87 - 0
tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php

@@ -411,4 +411,91 @@ class Foo
             ],
         ];
     }
+
+    /**
+     * @param array<int, int> $expectedTokens
+     *
+     * @dataProvider provideProcess82Cases
+     *
+     * @requires PHP 8.2
+     */
+    public function testProcess82(string $source, array $expectedTokens): void
+    {
+        $this->doTest($source, $expectedTokens);
+    }
+
+    public static function provideProcess82Cases(): iterable
+    {
+        yield 'disjunctive normal form types parameter' => [
+            '<?php function foo((A&B)|D $x): void {}',
+            [
+                10 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
+
+        yield 'disjunctive normal form types return' => [
+            '<?php function foo(): (A&B)|D {}',
+            [
+                13 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
+
+        yield 'disjunctive normal form types parameters' => [
+            '<?php function foo(
+                (A&B)|C|D $x,
+                A|(B&C)|D $y,
+                A|B|(C&D) $z,
+            ): void {}',
+            [
+                11 => CT::T_TYPE_ALTERNATION,
+                13 => CT::T_TYPE_ALTERNATION,
+                20 => CT::T_TYPE_ALTERNATION,
+                26 => CT::T_TYPE_ALTERNATION,
+                33 => CT::T_TYPE_ALTERNATION,
+                35 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
+
+        yield 'bigger set of multiple DNF properties' => [
+            '<?php
+class Dnf
+{
+    public A|(C&D) $a;
+    protected (C&D)|B $b;
+    private (C&D)|(E&F)|(G&H) $c;
+    static (C&D)|Z $d;
+    public /* */ (C&D)|X $e;
+
+    public function foo($a, $b) {
+        return
+            $z|($A&$B)|(A::z&B\A::x)
+            || A::b|($A&$B)
+        ;
+    }
+}
+',
+            [
+                10 => CT::T_TYPE_ALTERNATION,
+                27 => CT::T_TYPE_ALTERNATION,
+                40 => CT::T_TYPE_ALTERNATION,
+                46 => CT::T_TYPE_ALTERNATION,
+                63 => CT::T_TYPE_ALTERNATION,
+                78 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
+
+        yield 'arrow function with DNF types' => [
+            '<?php
+                $f1 = fn (): A|(B&C) => new Foo();
+                $f2 = fn ((A&B)|C $x, A|(B&C) $y): (A&B&C)|D|(E&F) => new Bar();
+            ',
+            [
+                13 => CT::T_TYPE_ALTERNATION,
+                41 => CT::T_TYPE_ALTERNATION,
+                48 => CT::T_TYPE_ALTERNATION,
+                66 => CT::T_TYPE_ALTERNATION,
+                68 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
+    }
 }

+ 112 - 0
tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php

@@ -57,6 +57,7 @@ final class TypeIntersectionTransformerTest extends AbstractTransformerTestCase
                 $x = ($y&$z);
                 function foo(){}
                 $a = $b&$c;
+                $a &+ $b;
             ',
         ];
 
@@ -323,4 +324,115 @@ function f( #[Target(\'a\')] #[Target(\'b\')] #[Target(\'c\')] #[Target(\'d\')]
             ],
         ];
     }
+
+    /**
+     * @param array<int, int> $expectedTokens
+     *
+     * @dataProvider provideProcess82Cases
+     *
+     * @requires PHP 8.2
+     */
+    public function testProcess82(string $source, array $expectedTokens): void
+    {
+        $this->doTest($source, $expectedTokens);
+    }
+
+    public static function provideProcess82Cases(): iterable
+    {
+        yield 'disjunctive normal form types parameter' => [
+            '<?php function foo((A&B)|D $x): void {}',
+            [
+                7 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
+
+        yield 'disjunctive normal form types return' => [
+            '<?php function foo(): (A&B)|D {}',
+            [
+                10 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
+
+        yield 'disjunctive normal form types parameters' => [
+            '<?php function foo(
+                (A&B)|C|D $x,
+                A|(B&C)|D $y,
+                (A&B)|(C&D) $z,
+            ): void {}',
+            [
+                8 => CT::T_TYPE_INTERSECTION,
+                23 => CT::T_TYPE_INTERSECTION,
+                34 => CT::T_TYPE_INTERSECTION,
+                40 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
+
+        yield 'lambda with lots of DNF parameters and some others' => [
+            '<?php
+$a = function(
+    (X&Y)|C $a,
+    $b = array(1,2),
+    (\X&\Y)|C $c,
+    array $d = [1,2],
+    (\X&\Y)|C $e,
+    $x, $y, $z, P|(H&J) $uu,
+) {};
+
+function foo (array $a = array(66,88, $d = [99,44],array()), $e = [99,44],(C&V)|G|array $f = array()){};
+
+return new static();
+',
+            [
+                10 => CT::T_TYPE_INTERSECTION, // $a
+                34 => CT::T_TYPE_INTERSECTION, // $c
+                60 => CT::T_TYPE_INTERSECTION, // $e
+                83 => CT::T_TYPE_INTERSECTION, // $uu
+                142 => CT::T_TYPE_INTERSECTION, // $f
+            ],
+        ];
+
+        yield 'bigger set of multiple DNF properties' => [
+            '<?php
+class Dnf
+{
+    public A|(C&D) $a;
+    protected (C&D)|B $b;
+    private (C&D)|(E&F)|(G&H) $c;
+    static (C&D)|Z $d;
+    public /* */ (C&D)|X $e;
+
+    public function foo($a, $b) {
+        return
+            $z|($A&$B)|(A::z&B\A::x)
+            || A::b|($A&$B)
+        ;
+    }
+}
+',
+            [
+                13 => CT::T_TYPE_INTERSECTION,
+                24 => CT::T_TYPE_INTERSECTION,
+                37 => CT::T_TYPE_INTERSECTION,
+                43 => CT::T_TYPE_INTERSECTION,
+                49 => CT::T_TYPE_INTERSECTION,
+                60 => CT::T_TYPE_INTERSECTION,
+                75 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
+
+        yield 'arrow function with DNF types' => [
+            '<?php
+                $f1 = fn (): A|(B&C) => new Foo();
+                $f2 = fn ((A&B)|C $x, A|(B&C) $y): (A&B&C)|D|(E&F) => new Bar();
+            ',
+            [
+                16 => CT::T_TYPE_INTERSECTION,
+                38 => CT::T_TYPE_INTERSECTION,
+                51 => CT::T_TYPE_INTERSECTION,
+                61 => CT::T_TYPE_INTERSECTION,
+                63 => CT::T_TYPE_INTERSECTION,
+                71 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
+    }
 }