Skip to content

Commit 0fa0e01

Browse files
committed
PHP 8.0 | Tokenizer/PHP: add support for match expressions in combination with arrow functions
As a match expression can be nested in an arrow function and visa versa, setting the scope openers/closers becomes _interesting_. If the arrow function would be allowed to take over, it would break the scope setting for match expression, making the `scope_closer` and `scope_condition` indexes next to useless. So in the interest of making things easier for sniff writers and keeping in mind that scope setting for arrow functions isn't perfect anyway, preference is given to keeping the scope setting for match structures intact. This means that if a match expression is in the return value of an arrow function, like so: ```php $fn = fn($x) => match($y) {...}; ``` ... the token _after_ the closing curly belonging to the `match` will be considered the scope closer for the arrow function. In this example, that is the `;` (semicolon). While, if an arrow function is the return value of one of the match expression cases, generally speaking the comma at the end of the case will be the scope closer for the arrow function. There is one exception to this: the comma after each case is optional for the last case in a match expression. In that situation, the last non-empty token will be considered the scope closer for the arrow function. Example: ```php $match = match($y) { default => fn($x) => $y * $x }; ``` In this example, the `$x` at the end of the arrow expression would be considered the scope closer for the arrow function. ```php $match = match($y) { default => fn($x) => ($y * $x) }; ``` And in this example, the parenthesis closer at the end of the arrow expression would be considered the scope closer for the arrow function.
1 parent 694c569 commit 0fa0e01

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

src/Tokenizers/PHP.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,6 +2242,31 @@ protected function processAdditional()
22422242
$lastEndToken = null;
22432243

22442244
for ($scopeCloser = ($arrow + 1); $scopeCloser < $numTokens; $scopeCloser++) {
2245+
// Arrow function closer should never be shared with the closer of a match
2246+
// control structure.
2247+
if (isset($this->tokens[$scopeCloser]['scope_closer'], $this->tokens[$scopeCloser]['scope_condition']) === true
2248+
&& $scopeCloser === $this->tokens[$scopeCloser]['scope_closer']
2249+
&& $this->tokens[$this->tokens[$scopeCloser]['scope_condition']]['code'] === T_MATCH
2250+
) {
2251+
if ($arrow < $this->tokens[$scopeCloser]['scope_condition']) {
2252+
// Match in return value of arrow function. Move on to the next token.
2253+
continue;
2254+
}
2255+
2256+
// Arrow function as return value for the last match case without trailing comma.
2257+
if ($lastEndToken !== null) {
2258+
$scopeCloser = $lastEndToken;
2259+
break;
2260+
}
2261+
2262+
for ($lastNonEmpty = ($scopeCloser - 1); $lastNonEmpty > $arrow; $lastNonEmpty--) {
2263+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$lastNonEmpty]['code']]) === false) {
2264+
$scopeCloser = $lastNonEmpty;
2265+
break 2;
2266+
}
2267+
}
2268+
}
2269+
22452270
if (isset($endTokens[$this->tokens[$scopeCloser]['code']]) === true) {
22462271
if ($lastEndToken !== null
22472272
&& $this->tokens[$scopeCloser]['code'] === T_CLOSE_PARENTHESIS

tests/Core/Tokenizer/BackfillFnTokenTest.inc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,46 @@ $arrowWithUnionReturn = fn($param) : int|float => $param | 10;
9090
/* testTernary */
9191
$fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b';
9292

93+
function matchInArrow($x) {
94+
/* testWithMatchValue */
95+
$fn = fn($x) => match(true) {
96+
1, 2, 3, 4, 5 => 'foo',
97+
default => 'bar',
98+
};
99+
}
100+
101+
function matchInArrowAndMore($x) {
102+
/* testWithMatchValueAndMore */
103+
$fn = fn($x) => match(true) {
104+
1, 2, 3, 4, 5 => 'foo',
105+
default => 'bar',
106+
} . 'suffix';
107+
}
108+
109+
function arrowFunctionInMatchWithTrailingComma($x) {
110+
return match ($x) {
111+
/* testInMatchNotLastValue */
112+
1 => fn($y) => callMe($y),
113+
/* testInMatchLastValueWithTrailingComma */
114+
default => fn($y) => callThem($y),
115+
};
116+
}
117+
118+
function arrowFunctionInMatchNoTrailingComma1($x) {
119+
return match ($x) {
120+
1 => fn($y) => callMe($y),
121+
/* testInMatchLastValueNoTrailingComma1 */
122+
default => fn($y) => callThem($y)
123+
};
124+
}
125+
126+
function arrowFunctionInMatchNoTrailingComma2($x) {
127+
return match ($x) {
128+
/* testInMatchLastValueNoTrailingComma2 */
129+
default => fn($y) => 5 * $y
130+
};
131+
}
132+
93133
/* testConstantDeclaration */
94134
const FN = 'a';
95135

tests/Core/Tokenizer/BackfillFnTokenTest.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,109 @@ public function testTernary()
465465
}//end testTernary()
466466

467467

468+
/**
469+
* Test arrow function returning a match control structure.
470+
*
471+
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
472+
*
473+
* @return void
474+
*/
475+
public function testWithMatchValue()
476+
{
477+
$token = $this->getTargetToken('/* testWithMatchValue */', T_FN);
478+
$this->backfillHelper($token);
479+
$this->scopePositionTestHelper($token, 5, 44);
480+
481+
}//end testWithMatchValue()
482+
483+
484+
/**
485+
* Test arrow function returning a match control structure with something behind it.
486+
*
487+
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
488+
*
489+
* @return void
490+
*/
491+
public function testWithMatchValueAndMore()
492+
{
493+
$token = $this->getTargetToken('/* testWithMatchValueAndMore */', T_FN);
494+
$this->backfillHelper($token);
495+
$this->scopePositionTestHelper($token, 5, 48);
496+
497+
}//end testWithMatchValueAndMore()
498+
499+
500+
/**
501+
* Test match control structure returning arrow functions.
502+
*
503+
* @param string $testMarker The comment prefacing the target token.
504+
* @param int $openerOffset The expected offset of the scope opener in relation to the testMarker.
505+
* @param int $closerOffset The expected offset of the scope closer in relation to the testMarker.
506+
* @param string $expectedCloserType The type of token expected for the scope closer.
507+
* @param string $expectedCloserFriendlyName A friendly name for the type of token expected for the scope closer
508+
* to be used in the error message for failing tests.
509+
*
510+
* @dataProvider dataInMatchValue
511+
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
512+
*
513+
* @return void
514+
*/
515+
public function testInMatchValue($testMarker, $openerOffset, $closerOffset, $expectedCloserType, $expectedCloserFriendlyName)
516+
{
517+
$tokens = self::$phpcsFile->getTokens();
518+
519+
$token = $this->getTargetToken($testMarker, T_FN);
520+
$this->backfillHelper($token);
521+
$this->scopePositionTestHelper($token, $openerOffset, $closerOffset, $expectedCloserFriendlyName);
522+
523+
$this->assertSame($expectedCloserType, $tokens[($token + $closerOffset)]['type'], 'Mismatched scope closer type');
524+
525+
}//end testInMatchValue()
526+
527+
528+
/**
529+
* Data provider.
530+
*
531+
* @see testInMatchValue()
532+
*
533+
* @return array
534+
*/
535+
public function dataInMatchValue()
536+
{
537+
return [
538+
'not_last_value' => [
539+
'/* testInMatchNotLastValue */',
540+
5,
541+
11,
542+
'T_COMMA',
543+
'comma',
544+
],
545+
'last_value_with_trailing_comma' => [
546+
'/* testInMatchLastValueWithTrailingComma */',
547+
5,
548+
11,
549+
'T_COMMA',
550+
'comma',
551+
],
552+
'last_value_without_trailing_comma_1' => [
553+
'/* testInMatchLastValueNoTrailingComma1 */',
554+
5,
555+
10,
556+
'T_CLOSE_PARENTHESIS',
557+
'close parenthesis',
558+
],
559+
'last_value_without_trailing_comma_2' => [
560+
'/* testInMatchLastValueNoTrailingComma2 */',
561+
5,
562+
11,
563+
'T_VARIABLE',
564+
'$y variable',
565+
],
566+
];
567+
568+
}//end dataInMatchValue()
569+
570+
468571
/**
469572
* Test arrow function nested within a method declaration.
470573
*

0 commit comments

Comments
 (0)