Skip to content

Commit

Permalink
minor: PHP8.2 - handle union and intersection types for DNF types (#6804
Browse files Browse the repository at this point in the history
)

Co-authored-by: Dariusz Ruminski <dariusz.ruminski@gmail.com>
  • Loading branch information
kubawerlos and keradus authored Mar 11, 2023
1 parent f123931 commit e561bd1
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 55 deletions.
81 changes: 26 additions & 55 deletions src/Tokenizer/AbstractTypeTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,76 +21,47 @@
*/
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
{
if (!$tokens[$index]->equals($originalToken)) {
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 changes: 12 additions & 0 deletions tests/Fixtures/Integration/misc/PHP8_2.test
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 changes: 47 additions & 0 deletions tests/Tokenizer/TokensAnalyzerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,53 @@ public static function provideIsBinaryOperator81Cases(): iterable
];
}

/**
* @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
*/
Expand Down
87 changes: 87 additions & 0 deletions tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,91 @@ public static function provideProcess81Cases(): iterable
],
];
}

/**
* @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 changes: 112 additions & 0 deletions tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static function provideProcessCases(): iterable
$x = ($y&$z);
function foo(){}
$a = $b&$c;
$a &+ $b;
',
];

Expand Down Expand Up @@ -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,
],
];
}
}

0 comments on commit e561bd1

Please sign in to comment.