Skip to content

Commit e561bd1

Browse files
kubawerloskeradus
andauthored
minor: PHP8.2 - handle union and intersection types for DNF types (#6804)
Co-authored-by: Dariusz Ruminski <[email protected]>
1 parent f123931 commit e561bd1

File tree

5 files changed

+284
-55
lines changed

5 files changed

+284
-55
lines changed

src/Tokenizer/AbstractTypeTransformer.php

Lines changed: 26 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,76 +21,47 @@
2121
*/
2222
abstract class AbstractTypeTransformer extends AbstractTransformer
2323
{
24+
private const TYPE_END_TOKENS = [')', [T_CALLABLE], [T_NS_SEPARATOR], [T_STRING], [CT::T_ARRAY_TYPEHINT]];
25+
26+
private const TYPE_TOKENS = [
27+
'|', '&', '(',
28+
...self::TYPE_END_TOKENS,
29+
[CT::T_TYPE_ALTERNATION], [CT::T_TYPE_INTERSECTION], // some siblings may already be transformed
30+
[T_WHITESPACE], [T_COMMENT], [T_DOC_COMMENT], // technically these can be inside of type tokens array
31+
];
32+
33+
abstract protected function replaceToken(Tokens $tokens, int $index): void;
34+
2435
/**
25-
* @param array{0: int, 1?: string}|string $originalToken
36+
* @param array{0: int, 1: string}|string $originalToken
2637
*/
2738
protected function doProcess(Tokens $tokens, int $index, $originalToken): void
2839
{
2940
if (!$tokens[$index]->equals($originalToken)) {
3041
return;
3142
}
3243

33-
$prevIndex = $this->getPreviousTokenCandidate($tokens, $index);
34-
35-
/** @var Token $prevToken */
36-
$prevToken = $tokens[$prevIndex];
37-
38-
if ($prevToken->isGivenKind([
39-
CT::T_TYPE_COLON, // `:` is part of a function return type `foo(): X|Y`
40-
CT::T_TYPE_ALTERNATION, // `|` is part of a union (chain) `X|Y`
41-
CT::T_TYPE_INTERSECTION,
42-
T_STATIC, T_VAR, T_PUBLIC, T_PROTECTED, T_PRIVATE, // `var X|Y $a;`, `private X|Y $a` or `public static X|Y $a`
43-
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, // promoted properties
44-
])) {
45-
$this->replaceToken($tokens, $index);
46-
47-
return;
48-
}
49-
50-
if (\defined('T_READONLY') && $prevToken->isGivenKind(T_READONLY)) { // @TODO: drop condition when PHP 8.1+ is required
51-
$this->replaceToken($tokens, $index);
52-
53-
return;
54-
}
55-
56-
if (!$prevToken->equalsAny(['(', ','])) {
57-
return;
58-
}
59-
60-
$prevPrevTokenIndex = $tokens->getPrevMeaningfulToken($prevIndex);
61-
62-
if ($tokens[$prevPrevTokenIndex]->isGivenKind(T_CATCH)) {
63-
$this->replaceToken($tokens, $index);
64-
65-
return;
66-
}
67-
68-
$functionKinds = [[T_FUNCTION], [T_FN]];
69-
$functionIndex = $tokens->getPrevTokenOfKind($prevIndex, $functionKinds);
70-
71-
if (null === $functionIndex) {
72-
return;
73-
}
74-
75-
$braceOpenIndex = $tokens->getNextTokenOfKind($functionIndex, ['(']);
76-
$braceCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $braceOpenIndex);
77-
78-
if ($braceCloseIndex < $index) {
44+
if (!$this->isPartOfType($tokens, $index)) {
7945
return;
8046
}
8147

8248
$this->replaceToken($tokens, $index);
8349
}
8450

85-
abstract protected function replaceToken(Tokens $tokens, int $index): void;
86-
87-
private function getPreviousTokenCandidate(Tokens $tokens, int $index): int
51+
private function isPartOfType(Tokens $tokens, int $index): bool
8852
{
89-
$candidateIndex = $tokens->getTokenNotOfKindsSibling($index, -1, [T_CALLABLE, T_NS_SEPARATOR, T_STRING, CT::T_ARRAY_TYPEHINT, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]);
53+
// for parameter there will be variable after type
54+
$variableIndex = $tokens->getTokenNotOfKindSibling($index, 1, self::TYPE_TOKENS);
55+
if ($tokens[$variableIndex]->isGivenKind(T_VARIABLE)) {
56+
return $tokens[$tokens->getPrevMeaningfulToken($variableIndex)]->equalsAny(self::TYPE_END_TOKENS);
57+
}
58+
59+
// return types and non-capturing catches
60+
$typeColonIndex = $tokens->getTokenNotOfKindSibling($index, -1, self::TYPE_TOKENS);
61+
if ($tokens[$typeColonIndex]->isGivenKind([T_CATCH, CT::T_TYPE_COLON])) {
62+
return true;
63+
}
9064

91-
return $tokens[$candidateIndex]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)
92-
? $this->getPreviousTokenCandidate($tokens, $tokens->getPrevTokenOfKind($index, [[T_ATTRIBUTE]]))
93-
: $candidateIndex
94-
;
65+
return false;
9566
}
9667
}

tests/Fixtures/Integration/misc/PHP8_2.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ trait WithConstants
101101
private const THREE = 'three';
102102
}
103103

104+
// https://wiki.php.net/rfc/dnf_types
105+
function generateSlug((HasTitle&HasId)|null $post)
106+
{
107+
throw new \Exception('not implemented');
108+
}
109+
104110
--INPUT--
105111
<?php
106112

@@ -191,3 +197,9 @@ trait WithConstants {
191197
protected const TWO = 'two';
192198
private const THREE = 'three';
193199
}
200+
201+
// https://wiki.php.net/rfc/dnf_types
202+
function generateSlug((HasTitle&HasId)|null $post)
203+
{
204+
throw new \Exception('not implemented');
205+
}

tests/Tokenizer/TokensAnalyzerTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,53 @@ public static function provideIsBinaryOperator81Cases(): iterable
19091909
];
19101910
}
19111911

1912+
/**
1913+
* @param array<int, bool> $expected
1914+
*
1915+
* @dataProvider provideIsBinaryOperator82Cases
1916+
*
1917+
* @requires PHP 8.2
1918+
*/
1919+
public function testIsBinaryOperator82(array $expected, string $source): void
1920+
{
1921+
$tokens = Tokens::fromCode($source);
1922+
$tokensAnalyzer = new TokensAnalyzer($tokens);
1923+
1924+
foreach ($tokens as $index => $token) {
1925+
$isBinary = isset($expected[$index]);
1926+
static::assertSame($isBinary, $tokensAnalyzer->isBinaryOperator($index));
1927+
if ($isBinary) {
1928+
static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index));
1929+
static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index));
1930+
}
1931+
}
1932+
}
1933+
1934+
public static function provideIsBinaryOperator82Cases(): iterable
1935+
{
1936+
yield [
1937+
[],
1938+
'<?php class Dnf { public static I|(P&S11) $f2;}',
1939+
];
1940+
1941+
yield [
1942+
[],
1943+
'<?php function Foo((A&B)|I $x): (X&Z)|(p\f\G&Y\Z)|z { return foo();}',
1944+
];
1945+
1946+
$particularEndOfFile = 'A|(B&C); }';
1947+
1948+
yield sprintf('block "%s" at the end of file that is a type', $particularEndOfFile) => [
1949+
[],
1950+
'<?php abstract class A { abstract function foo(): '.$particularEndOfFile,
1951+
];
1952+
1953+
yield sprintf('block "%s" at the end of file that is not a type', $particularEndOfFile) => [
1954+
[12 => true, 15 => true],
1955+
'<?php function foo() { return '.$particularEndOfFile,
1956+
];
1957+
}
1958+
19121959
/**
19131960
* @dataProvider provideArrayExceptionsCases
19141961
*/

tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,91 @@ public static function provideProcess81Cases(): iterable
411411
],
412412
];
413413
}
414+
415+
/**
416+
* @param array<int, int> $expectedTokens
417+
*
418+
* @dataProvider provideProcess82Cases
419+
*
420+
* @requires PHP 8.2
421+
*/
422+
public function testProcess82(string $source, array $expectedTokens): void
423+
{
424+
$this->doTest($source, $expectedTokens);
425+
}
426+
427+
public static function provideProcess82Cases(): iterable
428+
{
429+
yield 'disjunctive normal form types parameter' => [
430+
'<?php function foo((A&B)|D $x): void {}',
431+
[
432+
10 => CT::T_TYPE_ALTERNATION,
433+
],
434+
];
435+
436+
yield 'disjunctive normal form types return' => [
437+
'<?php function foo(): (A&B)|D {}',
438+
[
439+
13 => CT::T_TYPE_ALTERNATION,
440+
],
441+
];
442+
443+
yield 'disjunctive normal form types parameters' => [
444+
'<?php function foo(
445+
(A&B)|C|D $x,
446+
A|(B&C)|D $y,
447+
A|B|(C&D) $z,
448+
): void {}',
449+
[
450+
11 => CT::T_TYPE_ALTERNATION,
451+
13 => CT::T_TYPE_ALTERNATION,
452+
20 => CT::T_TYPE_ALTERNATION,
453+
26 => CT::T_TYPE_ALTERNATION,
454+
33 => CT::T_TYPE_ALTERNATION,
455+
35 => CT::T_TYPE_ALTERNATION,
456+
],
457+
];
458+
459+
yield 'bigger set of multiple DNF properties' => [
460+
'<?php
461+
class Dnf
462+
{
463+
public A|(C&D) $a;
464+
protected (C&D)|B $b;
465+
private (C&D)|(E&F)|(G&H) $c;
466+
static (C&D)|Z $d;
467+
public /* */ (C&D)|X $e;
468+
469+
public function foo($a, $b) {
470+
return
471+
$z|($A&$B)|(A::z&B\A::x)
472+
|| A::b|($A&$B)
473+
;
474+
}
475+
}
476+
',
477+
[
478+
10 => CT::T_TYPE_ALTERNATION,
479+
27 => CT::T_TYPE_ALTERNATION,
480+
40 => CT::T_TYPE_ALTERNATION,
481+
46 => CT::T_TYPE_ALTERNATION,
482+
63 => CT::T_TYPE_ALTERNATION,
483+
78 => CT::T_TYPE_ALTERNATION,
484+
],
485+
];
486+
487+
yield 'arrow function with DNF types' => [
488+
'<?php
489+
$f1 = fn (): A|(B&C) => new Foo();
490+
$f2 = fn ((A&B)|C $x, A|(B&C) $y): (A&B&C)|D|(E&F) => new Bar();
491+
',
492+
[
493+
13 => CT::T_TYPE_ALTERNATION,
494+
41 => CT::T_TYPE_ALTERNATION,
495+
48 => CT::T_TYPE_ALTERNATION,
496+
66 => CT::T_TYPE_ALTERNATION,
497+
68 => CT::T_TYPE_ALTERNATION,
498+
],
499+
];
500+
}
414501
}

tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public static function provideProcessCases(): iterable
5757
$x = ($y&$z);
5858
function foo(){}
5959
$a = $b&$c;
60+
$a &+ $b;
6061
',
6162
];
6263

@@ -323,4 +324,115 @@ function f( #[Target(\'a\')] #[Target(\'b\')] #[Target(\'c\')] #[Target(\'d\')]
323324
],
324325
];
325326
}
327+
328+
/**
329+
* @param array<int, int> $expectedTokens
330+
*
331+
* @dataProvider provideProcess82Cases
332+
*
333+
* @requires PHP 8.2
334+
*/
335+
public function testProcess82(string $source, array $expectedTokens): void
336+
{
337+
$this->doTest($source, $expectedTokens);
338+
}
339+
340+
public static function provideProcess82Cases(): iterable
341+
{
342+
yield 'disjunctive normal form types parameter' => [
343+
'<?php function foo((A&B)|D $x): void {}',
344+
[
345+
7 => CT::T_TYPE_INTERSECTION,
346+
],
347+
];
348+
349+
yield 'disjunctive normal form types return' => [
350+
'<?php function foo(): (A&B)|D {}',
351+
[
352+
10 => CT::T_TYPE_INTERSECTION,
353+
],
354+
];
355+
356+
yield 'disjunctive normal form types parameters' => [
357+
'<?php function foo(
358+
(A&B)|C|D $x,
359+
A|(B&C)|D $y,
360+
(A&B)|(C&D) $z,
361+
): void {}',
362+
[
363+
8 => CT::T_TYPE_INTERSECTION,
364+
23 => CT::T_TYPE_INTERSECTION,
365+
34 => CT::T_TYPE_INTERSECTION,
366+
40 => CT::T_TYPE_INTERSECTION,
367+
],
368+
];
369+
370+
yield 'lambda with lots of DNF parameters and some others' => [
371+
'<?php
372+
$a = function(
373+
(X&Y)|C $a,
374+
$b = array(1,2),
375+
(\X&\Y)|C $c,
376+
array $d = [1,2],
377+
(\X&\Y)|C $e,
378+
$x, $y, $z, P|(H&J) $uu,
379+
) {};
380+
381+
function foo (array $a = array(66,88, $d = [99,44],array()), $e = [99,44],(C&V)|G|array $f = array()){};
382+
383+
return new static();
384+
',
385+
[
386+
10 => CT::T_TYPE_INTERSECTION, // $a
387+
34 => CT::T_TYPE_INTERSECTION, // $c
388+
60 => CT::T_TYPE_INTERSECTION, // $e
389+
83 => CT::T_TYPE_INTERSECTION, // $uu
390+
142 => CT::T_TYPE_INTERSECTION, // $f
391+
],
392+
];
393+
394+
yield 'bigger set of multiple DNF properties' => [
395+
'<?php
396+
class Dnf
397+
{
398+
public A|(C&D) $a;
399+
protected (C&D)|B $b;
400+
private (C&D)|(E&F)|(G&H) $c;
401+
static (C&D)|Z $d;
402+
public /* */ (C&D)|X $e;
403+
404+
public function foo($a, $b) {
405+
return
406+
$z|($A&$B)|(A::z&B\A::x)
407+
|| A::b|($A&$B)
408+
;
409+
}
410+
}
411+
',
412+
[
413+
13 => CT::T_TYPE_INTERSECTION,
414+
24 => CT::T_TYPE_INTERSECTION,
415+
37 => CT::T_TYPE_INTERSECTION,
416+
43 => CT::T_TYPE_INTERSECTION,
417+
49 => CT::T_TYPE_INTERSECTION,
418+
60 => CT::T_TYPE_INTERSECTION,
419+
75 => CT::T_TYPE_INTERSECTION,
420+
],
421+
];
422+
423+
yield 'arrow function with DNF types' => [
424+
'<?php
425+
$f1 = fn (): A|(B&C) => new Foo();
426+
$f2 = fn ((A&B)|C $x, A|(B&C) $y): (A&B&C)|D|(E&F) => new Bar();
427+
',
428+
[
429+
16 => CT::T_TYPE_INTERSECTION,
430+
38 => CT::T_TYPE_INTERSECTION,
431+
51 => CT::T_TYPE_INTERSECTION,
432+
61 => CT::T_TYPE_INTERSECTION,
433+
63 => CT::T_TYPE_INTERSECTION,
434+
71 => CT::T_TYPE_INTERSECTION,
435+
],
436+
];
437+
}
326438
}

0 commit comments

Comments
 (0)