Skip to content

Commit b5b1c57

Browse files
authored
Merge pull request #143 from PHPCSStandards/functiondeclarations/sync-arrow-functions-with-phpcs-356
Improve support for arrow functions / sync with phpcs 3.5.5/6
2 parents 66a48e9 + 2d91603 commit b5b1c57

File tree

8 files changed

+287
-3
lines changed

8 files changed

+287
-3
lines changed

PHPCSUtils/BackCompat/BCFile.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,7 @@ public static function findStartOfStatement(File $phpcsFile, $start, $ignore = n
12511251
* - PHPCS 3.5.0: Improved handling of group use statements.
12521252
* - PHPCS 3.5.3: Added support for PHP 7.4 T_FN arrow functions.
12531253
* - PHPCS 3.5.4: Improved support for PHP 7.4 T_FN arrow functions.
1254+
* - PHPCS 3.5.5: Improved support for PHP 7.4 T_FN arrow functions, PHPCS #2895.
12541255
*
12551256
* @see \PHP_CodeSniffer\Files\File::findEndOfStatement() Original source.
12561257
*
@@ -1288,7 +1289,6 @@ public static function findEndOfStatement(File $phpcsFile, $start, $ignore = nul
12881289
}
12891290

12901291
$lastNotEmpty = $start;
1291-
12921292
for ($i = $start; $i < $phpcsFile->numTokens; $i++) {
12931293
if ($i !== $start && isset($endTokens[$tokens[$i]['code']]) === true) {
12941294
// Found the end of the statement.
@@ -1311,6 +1311,8 @@ public static function findEndOfStatement(File $phpcsFile, $start, $ignore = nul
13111311
|| $i === $tokens[$i]['scope_condition'])
13121312
) {
13131313
if ($tokens[$i]['type'] === 'T_FN') {
1314+
$lastNotEmpty = $tokens[$i]['scope_closer'];
1315+
13141316
// Minus 1 as the closer can be shared.
13151317
$i = ($tokens[$i]['scope_closer'] - 1);
13161318
continue;
@@ -1342,8 +1344,11 @@ public static function findEndOfStatement(File $phpcsFile, $start, $ignore = nul
13421344
return $arrowFunctionOpenClose['scope_closer'];
13431345
}
13441346

1347+
$lastNotEmpty = $arrowFunctionOpenClose['scope_closer'];
1348+
13451349
// Minus 1 as the closer can be shared.
13461350
$i = ($arrowFunctionOpenClose['scope_closer'] - 1);
1351+
continue;
13471352
}
13481353
}
13491354

PHPCSUtils/Utils/FunctionDeclarations.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
676676

677677
if ($tokens[$stackPtr]['type'] === 'T_FN'
678678
&& isset($tokens[$stackPtr]['scope_closer']) === true
679+
&& \version_compare(Helper::getVersion(), '3.5.4', '>') === true
679680
) {
680681
// The keys will either all be set or none will be set, so no additional checks needed.
681682
return [
@@ -736,12 +737,20 @@ public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
736737

737738
$returnValue['scope_opener'] = $arrow;
738739
$inTernary = false;
740+
$lastEndToken = null;
739741

740742
for ($scopeCloser = ($arrow + 1); $scopeCloser < $phpcsFile->numTokens; $scopeCloser++) {
741743
if (isset(self::$arrowFunctionEndTokens[$tokens[$scopeCloser]['code']]) === true
742744
// BC for misidentified ternary else in some PHPCS versions.
743745
&& ($tokens[$scopeCloser]['code'] !== \T_COLON || $inTernary === false)
744746
) {
747+
if ($lastEndToken !== null
748+
&& $tokens[$scopeCloser]['code'] === \T_CLOSE_PARENTHESIS
749+
&& $tokens[$scopeCloser]['parenthesis_opener'] < $arrow
750+
) {
751+
$scopeCloser = $lastEndToken;
752+
}
753+
745754
break;
746755
}
747756

@@ -756,19 +765,23 @@ public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
756765

757766
if (isset($tokens[$scopeCloser]['scope_closer']) === true
758767
&& $tokens[$scopeCloser]['code'] !== \T_INLINE_ELSE
768+
&& $tokens[$scopeCloser]['code'] !== \T_END_HEREDOC
769+
&& $tokens[$scopeCloser]['code'] !== \T_END_NOWDOC
759770
) {
760771
// We minus 1 here in case the closer can be shared with us.
761772
$scopeCloser = ($tokens[$scopeCloser]['scope_closer'] - 1);
762773
continue;
763774
}
764775

765776
if (isset($tokens[$scopeCloser]['parenthesis_closer']) === true) {
766-
$scopeCloser = $tokens[$scopeCloser]['parenthesis_closer'];
777+
$scopeCloser = $tokens[$scopeCloser]['parenthesis_closer'];
778+
$lastEndToken = $scopeCloser;
767779
continue;
768780
}
769781

770782
if (isset($tokens[$scopeCloser]['bracket_closer']) === true) {
771-
$scopeCloser = $tokens[$scopeCloser]['bracket_closer'];
783+
$scopeCloser = $tokens[$scopeCloser]['bracket_closer'];
784+
$lastEndToken = $scopeCloser;
772785
continue;
773786
}
774787

Tests/BackCompat/BCFile/FindEndOfStatementTest.inc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,14 @@ static fn ($a) => $a;
4242
/* testArrowFunctionReturnValue */
4343
fn(): array => [a($a, $b)];
4444

45+
$foo = foo(
46+
/* testArrowFunctionAsArgument */
47+
fn() => bar()
48+
);
49+
50+
$foo = foo(
51+
/* testArrowFunctionWithArrayAsArgument */
52+
fn() => [$row[0], $row[3]]
53+
);
54+
4555
return 0;

Tests/BackCompat/BCFile/FindEndOfStatementTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,33 @@ public function testArrowFunctionReturnValue()
207207

208208
$this->assertSame(($start + 18), $found);
209209
}
210+
211+
/**
212+
* Test arrow function used as a function argument.
213+
*
214+
* @return void
215+
*/
216+
public function testArrowFunctionAsArgument()
217+
{
218+
$start = $this->getTargetToken('/* testArrowFunctionAsArgument */', Collections::arrowFunctionTokensBC());
219+
$found = BCFile::findEndOfStatement(self::$phpcsFile, $start);
220+
221+
$this->assertSame(($start + 8), $found);
222+
}
223+
224+
/**
225+
* Test arrow function with arrays used as a function argument.
226+
*
227+
* @return void
228+
*/
229+
public function testArrowFunctionWithArrayAsArgument()
230+
{
231+
$start = $this->getTargetToken(
232+
'/* testArrowFunctionWithArrayAsArgument */',
233+
Collections::arrowFunctionTokensBC()
234+
);
235+
$found = BCFile::findEndOfStatement(self::$phpcsFile, $start);
236+
237+
$this->assertSame(($start + 17), $found);
238+
}
210239
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
/* testHeredoc */
4+
$fn1 = fn() => <<<HTML
5+
fn
6+
HTML;
7+
8+
/* testNowdoc */
9+
$fn1 = fn() => <<<'HTML'
10+
fn
11+
HTML;
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2019-2020 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Tests\Utils\FunctionDeclarations;
12+
13+
use PHPCSUtils\BackCompat\Helper;
14+
use PHPCSUtils\TestUtils\UtilityMethodTestCase;
15+
use PHPCSUtils\Tokens\Collections;
16+
use PHPCSUtils\Utils\FunctionDeclarations;
17+
18+
/**
19+
* Tests for the \PHPCSUtils\Utils\FunctionDeclarations::isArrowFunction() and the
20+
* \PHPCSUtils\Utils\FunctionDeclarations::getArrowFunctionOpenClose() methods for
21+
* a particular situation which will hang the tokenizer.
22+
*
23+
* These tests are based on the `Tokenizer/BackfillFnTokenTest` file in PHPCS itself.
24+
*
25+
* @link https://github.com/squizlabs/php_codesniffer/issues/2926
26+
*
27+
* @covers \PHPCSUtils\Utils\FunctionDeclarations::isArrowFunction
28+
* @covers \PHPCSUtils\Utils\FunctionDeclarations::getArrowFunctionOpenClose
29+
*
30+
* @group functiondeclarations
31+
*
32+
* @since 1.0.0
33+
*/
34+
class IsArrowFunction2926Test extends UtilityMethodTestCase
35+
{
36+
37+
/**
38+
* PHPCS versions in which the tokenizer will hang for these particular test cases.
39+
*
40+
* @var array
41+
*/
42+
private $unsupportedPHPCSVersions = [
43+
'3.5.3' => true,
44+
'3.5.4' => true,
45+
'3.5.5' => true,
46+
];
47+
48+
/**
49+
* Whether the test case file has been tokenized.
50+
*
51+
* Efficiency tweak as the tokenization is done in "before" not in "before class"
52+
* for this test.
53+
*
54+
* @var bool
55+
*/
56+
private static $tokenized = false;
57+
58+
/**
59+
* Do NOT Initialize PHPCS & tokenize the test case file.
60+
*
61+
* Skip tokenizing the test case file on "before class" as at that time, we can't skip the test
62+
* yet if the PHPCS version in incompatible and it would hang the Tokenizer (and therefore
63+
* the test) if it is.
64+
*
65+
* @beforeClass
66+
*
67+
* @return void
68+
*/
69+
public static function setUpTestFile()
70+
{
71+
// Skip the tokenizing of the test case file at this time.
72+
}
73+
74+
/**
75+
* Initialize PHPCS & tokenize the test case file on compatible PHPCS versions.
76+
*
77+
* Skip this test on PHPCS versions on which the Tokenizer will hang.
78+
*
79+
* @before
80+
*
81+
* @return void
82+
*/
83+
public function setUpTestFileForReal()
84+
{
85+
$phpcsVersion = Helper::getVersion();
86+
87+
if (isset($this->unsupportedPHPCSVersions[$phpcsVersion]) === true) {
88+
$this->markTestSkipped("Issue 2926 can not be tested on PHPCS $phpcsVersion as the Tokenizer will hang.");
89+
}
90+
91+
if (self::$tokenized === false) {
92+
parent::setUpTestFile();
93+
self::$tokenized = true;
94+
}
95+
}
96+
97+
/**
98+
* Test correctly detecting arrow functions.
99+
*
100+
* @dataProvider dataArrowFunction
101+
*
102+
* @param string $testMarker The comment which prefaces the target token in the test file.
103+
* @param array $expected The expected return value for the respective functions.
104+
* @param array $targetContent The content for the target token to look for in case there could
105+
* be confusion.
106+
*
107+
* @return void
108+
*/
109+
public function testIsArrowFunction($testMarker, $expected, $targetContent = null)
110+
{
111+
$targets = Collections::arrowFunctionTokensBC();
112+
$stackPtr = $this->getTargetToken($testMarker, $targets, $targetContent);
113+
$result = FunctionDeclarations::isArrowFunction(self::$phpcsFile, $stackPtr);
114+
$this->assertSame($expected['is'], $result);
115+
}
116+
117+
/**
118+
* Test correctly detecting arrow functions.
119+
*
120+
* @dataProvider dataArrowFunction
121+
*
122+
* @param string $testMarker The comment which prefaces the target token in the test file.
123+
* @param array $expected The expected return value for the respective functions.
124+
* @param string $targetContent The content for the target token to look for in case there could
125+
* be confusion.
126+
*
127+
* @return void
128+
*/
129+
public function testGetArrowFunctionOpenClose($testMarker, $expected, $targetContent = 'fn')
130+
{
131+
$targets = Collections::arrowFunctionTokensBC();
132+
$stackPtr = $this->getTargetToken($testMarker, $targets, $targetContent);
133+
134+
// Change from offsets to absolute token positions.
135+
if ($expected['get'] != false) {
136+
foreach ($expected['get'] as $key => $value) {
137+
$expected['get'][$key] += $stackPtr;
138+
}
139+
}
140+
141+
$result = FunctionDeclarations::getArrowFunctionOpenClose(self::$phpcsFile, $stackPtr);
142+
$this->assertSame($expected['get'], $result);
143+
}
144+
145+
/**
146+
* Data provider.
147+
*
148+
* @see testIsArrowFunction() For the array format.
149+
* @see testgetArrowFunctionOpenClose() For the array format.
150+
*
151+
* @return array
152+
*/
153+
public function dataArrowFunction()
154+
{
155+
return [
156+
'arrow-function-returning-heredoc' => [
157+
'/* testHeredoc */',
158+
[
159+
'is' => true,
160+
'get' => [
161+
'parenthesis_opener' => 1,
162+
'parenthesis_closer' => 2,
163+
'scope_opener' => 4,
164+
'scope_closer' => 9,
165+
],
166+
],
167+
],
168+
'arrow-function-returning-nowdoc' => [
169+
'/* testNowdoc */',
170+
[
171+
'is' => true,
172+
'get' => [
173+
'parenthesis_opener' => 1,
174+
'parenthesis_closer' => 2,
175+
'scope_opener' => 4,
176+
'scope_closer' => 9,
177+
],
178+
],
179+
],
180+
];
181+
}
182+
}

Tests/Utils/FunctionDeclarations/IsArrowFunctionTest.inc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ array_map(
8888
/* testTernary */
8989
$fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b';
9090

91+
$foo = foo(
92+
/* testArrowFunctionAsArgument */
93+
fn() => bar()
94+
);
95+
96+
$foo = foo(
97+
/* testArrowFunctionWithArrayAsArgument */
98+
fn() => [$row[0], $row[3]]
99+
);
100+
91101
/* testConstantDeclaration */
92102
const FN = 'a';
93103

Tests/Utils/FunctionDeclarations/IsArrowFunctionTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,30 @@ public function dataArrowFunction()
456456
],
457457
],
458458
],
459+
'arrow-function-as-function-call-argument' => [
460+
'/* testArrowFunctionAsArgument */',
461+
[
462+
'is' => true,
463+
'get' => [
464+
'parenthesis_opener' => 1,
465+
'parenthesis_closer' => 2,
466+
'scope_opener' => 4,
467+
'scope_closer' => 8,
468+
],
469+
],
470+
],
471+
'arrow-function-as-function-call-argument-with-array-return' => [
472+
'/* testArrowFunctionWithArrayAsArgument */',
473+
[
474+
'is' => true,
475+
'get' => [
476+
'parenthesis_opener' => 1,
477+
'parenthesis_closer' => 2,
478+
'scope_opener' => 4,
479+
'scope_closer' => 17,
480+
],
481+
],
482+
],
459483
'arrow-function-nested-in-method' => [
460484
'/* testNestedInMethod */',
461485
[

0 commit comments

Comments
 (0)