Skip to content

Commit

Permalink
Merge pull request #143 from PHPCSStandards/functiondeclarations/sync…
Browse files Browse the repository at this point in the history
…-arrow-functions-with-phpcs-356

Improve support for arrow functions / sync with phpcs 3.5.5/6
  • Loading branch information
jrfnl committed Jun 5, 2020
2 parents 66a48e9 + 2d91603 commit b5b1c57
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 3 deletions.
7 changes: 6 additions & 1 deletion PHPCSUtils/BackCompat/BCFile.php
Expand Up @@ -1251,6 +1251,7 @@ public static function findStartOfStatement(File $phpcsFile, $start, $ignore = n
* - PHPCS 3.5.0: Improved handling of group use statements.
* - PHPCS 3.5.3: Added support for PHP 7.4 T_FN arrow functions.
* - PHPCS 3.5.4: Improved support for PHP 7.4 T_FN arrow functions.
* - PHPCS 3.5.5: Improved support for PHP 7.4 T_FN arrow functions, PHPCS #2895.
*
* @see \PHP_CodeSniffer\Files\File::findEndOfStatement() Original source.
*
Expand Down Expand Up @@ -1288,7 +1289,6 @@ public static function findEndOfStatement(File $phpcsFile, $start, $ignore = nul
}

$lastNotEmpty = $start;

for ($i = $start; $i < $phpcsFile->numTokens; $i++) {
if ($i !== $start && isset($endTokens[$tokens[$i]['code']]) === true) {
// Found the end of the statement.
Expand All @@ -1311,6 +1311,8 @@ public static function findEndOfStatement(File $phpcsFile, $start, $ignore = nul
|| $i === $tokens[$i]['scope_condition'])
) {
if ($tokens[$i]['type'] === 'T_FN') {
$lastNotEmpty = $tokens[$i]['scope_closer'];

// Minus 1 as the closer can be shared.
$i = ($tokens[$i]['scope_closer'] - 1);
continue;
Expand Down Expand Up @@ -1342,8 +1344,11 @@ public static function findEndOfStatement(File $phpcsFile, $start, $ignore = nul
return $arrowFunctionOpenClose['scope_closer'];
}

$lastNotEmpty = $arrowFunctionOpenClose['scope_closer'];

// Minus 1 as the closer can be shared.
$i = ($arrowFunctionOpenClose['scope_closer'] - 1);
continue;
}
}

Expand Down
17 changes: 15 additions & 2 deletions PHPCSUtils/Utils/FunctionDeclarations.php
Expand Up @@ -676,6 +676,7 @@ public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)

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

$returnValue['scope_opener'] = $arrow;
$inTernary = false;
$lastEndToken = null;

for ($scopeCloser = ($arrow + 1); $scopeCloser < $phpcsFile->numTokens; $scopeCloser++) {
if (isset(self::$arrowFunctionEndTokens[$tokens[$scopeCloser]['code']]) === true
// BC for misidentified ternary else in some PHPCS versions.
&& ($tokens[$scopeCloser]['code'] !== \T_COLON || $inTernary === false)
) {
if ($lastEndToken !== null
&& $tokens[$scopeCloser]['code'] === \T_CLOSE_PARENTHESIS
&& $tokens[$scopeCloser]['parenthesis_opener'] < $arrow
) {
$scopeCloser = $lastEndToken;
}

break;
}

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

if (isset($tokens[$scopeCloser]['scope_closer']) === true
&& $tokens[$scopeCloser]['code'] !== \T_INLINE_ELSE
&& $tokens[$scopeCloser]['code'] !== \T_END_HEREDOC
&& $tokens[$scopeCloser]['code'] !== \T_END_NOWDOC
) {
// We minus 1 here in case the closer can be shared with us.
$scopeCloser = ($tokens[$scopeCloser]['scope_closer'] - 1);
continue;
}

if (isset($tokens[$scopeCloser]['parenthesis_closer']) === true) {
$scopeCloser = $tokens[$scopeCloser]['parenthesis_closer'];
$scopeCloser = $tokens[$scopeCloser]['parenthesis_closer'];
$lastEndToken = $scopeCloser;
continue;
}

if (isset($tokens[$scopeCloser]['bracket_closer']) === true) {
$scopeCloser = $tokens[$scopeCloser]['bracket_closer'];
$scopeCloser = $tokens[$scopeCloser]['bracket_closer'];
$lastEndToken = $scopeCloser;
continue;
}

Expand Down
10 changes: 10 additions & 0 deletions Tests/BackCompat/BCFile/FindEndOfStatementTest.inc
Expand Up @@ -42,4 +42,14 @@ static fn ($a) => $a;
/* testArrowFunctionReturnValue */
fn(): array => [a($a, $b)];

$foo = foo(
/* testArrowFunctionAsArgument */
fn() => bar()
);

$foo = foo(
/* testArrowFunctionWithArrayAsArgument */
fn() => [$row[0], $row[3]]
);

return 0;
29 changes: 29 additions & 0 deletions Tests/BackCompat/BCFile/FindEndOfStatementTest.php
Expand Up @@ -207,4 +207,33 @@ public function testArrowFunctionReturnValue()

$this->assertSame(($start + 18), $found);
}

/**
* Test arrow function used as a function argument.
*
* @return void
*/
public function testArrowFunctionAsArgument()
{
$start = $this->getTargetToken('/* testArrowFunctionAsArgument */', Collections::arrowFunctionTokensBC());
$found = BCFile::findEndOfStatement(self::$phpcsFile, $start);

$this->assertSame(($start + 8), $found);
}

/**
* Test arrow function with arrays used as a function argument.
*
* @return void
*/
public function testArrowFunctionWithArrayAsArgument()
{
$start = $this->getTargetToken(
'/* testArrowFunctionWithArrayAsArgument */',
Collections::arrowFunctionTokensBC()
);
$found = BCFile::findEndOfStatement(self::$phpcsFile, $start);

$this->assertSame(($start + 17), $found);
}
}
11 changes: 11 additions & 0 deletions Tests/Utils/FunctionDeclarations/IsArrowFunction2926Test.inc
@@ -0,0 +1,11 @@
<?php

/* testHeredoc */
$fn1 = fn() => <<<HTML
fn
HTML;

/* testNowdoc */
$fn1 = fn() => <<<'HTML'
fn
HTML;
182 changes: 182 additions & 0 deletions Tests/Utils/FunctionDeclarations/IsArrowFunction2926Test.php
@@ -0,0 +1,182 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2020 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Tests\Utils\FunctionDeclarations;

use PHPCSUtils\BackCompat\Helper;
use PHPCSUtils\TestUtils\UtilityMethodTestCase;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\FunctionDeclarations;

/**
* Tests for the \PHPCSUtils\Utils\FunctionDeclarations::isArrowFunction() and the
* \PHPCSUtils\Utils\FunctionDeclarations::getArrowFunctionOpenClose() methods for
* a particular situation which will hang the tokenizer.
*
* These tests are based on the `Tokenizer/BackfillFnTokenTest` file in PHPCS itself.
*
* @link https://github.com/squizlabs/php_codesniffer/issues/2926
*
* @covers \PHPCSUtils\Utils\FunctionDeclarations::isArrowFunction
* @covers \PHPCSUtils\Utils\FunctionDeclarations::getArrowFunctionOpenClose
*
* @group functiondeclarations
*
* @since 1.0.0
*/
class IsArrowFunction2926Test extends UtilityMethodTestCase
{

/**
* PHPCS versions in which the tokenizer will hang for these particular test cases.
*
* @var array
*/
private $unsupportedPHPCSVersions = [
'3.5.3' => true,
'3.5.4' => true,
'3.5.5' => true,
];

/**
* Whether the test case file has been tokenized.
*
* Efficiency tweak as the tokenization is done in "before" not in "before class"
* for this test.
*
* @var bool
*/
private static $tokenized = false;

/**
* Do NOT Initialize PHPCS & tokenize the test case file.
*
* Skip tokenizing the test case file on "before class" as at that time, we can't skip the test
* yet if the PHPCS version in incompatible and it would hang the Tokenizer (and therefore
* the test) if it is.
*
* @beforeClass
*
* @return void
*/
public static function setUpTestFile()
{
// Skip the tokenizing of the test case file at this time.
}

/**
* Initialize PHPCS & tokenize the test case file on compatible PHPCS versions.
*
* Skip this test on PHPCS versions on which the Tokenizer will hang.
*
* @before
*
* @return void
*/
public function setUpTestFileForReal()
{
$phpcsVersion = Helper::getVersion();

if (isset($this->unsupportedPHPCSVersions[$phpcsVersion]) === true) {
$this->markTestSkipped("Issue 2926 can not be tested on PHPCS $phpcsVersion as the Tokenizer will hang.");
}

if (self::$tokenized === false) {
parent::setUpTestFile();
self::$tokenized = true;
}
}

/**
* Test correctly detecting arrow functions.
*
* @dataProvider dataArrowFunction
*
* @param string $testMarker The comment which prefaces the target token in the test file.
* @param array $expected The expected return value for the respective functions.
* @param array $targetContent The content for the target token to look for in case there could
* be confusion.
*
* @return void
*/
public function testIsArrowFunction($testMarker, $expected, $targetContent = null)
{
$targets = Collections::arrowFunctionTokensBC();
$stackPtr = $this->getTargetToken($testMarker, $targets, $targetContent);
$result = FunctionDeclarations::isArrowFunction(self::$phpcsFile, $stackPtr);
$this->assertSame($expected['is'], $result);
}

/**
* Test correctly detecting arrow functions.
*
* @dataProvider dataArrowFunction
*
* @param string $testMarker The comment which prefaces the target token in the test file.
* @param array $expected The expected return value for the respective functions.
* @param string $targetContent The content for the target token to look for in case there could
* be confusion.
*
* @return void
*/
public function testGetArrowFunctionOpenClose($testMarker, $expected, $targetContent = 'fn')
{
$targets = Collections::arrowFunctionTokensBC();
$stackPtr = $this->getTargetToken($testMarker, $targets, $targetContent);

// Change from offsets to absolute token positions.
if ($expected['get'] != false) {
foreach ($expected['get'] as $key => $value) {
$expected['get'][$key] += $stackPtr;
}
}

$result = FunctionDeclarations::getArrowFunctionOpenClose(self::$phpcsFile, $stackPtr);
$this->assertSame($expected['get'], $result);
}

/**
* Data provider.
*
* @see testIsArrowFunction() For the array format.
* @see testgetArrowFunctionOpenClose() For the array format.
*
* @return array
*/
public function dataArrowFunction()
{
return [
'arrow-function-returning-heredoc' => [
'/* testHeredoc */',
[
'is' => true,
'get' => [
'parenthesis_opener' => 1,
'parenthesis_closer' => 2,
'scope_opener' => 4,
'scope_closer' => 9,
],
],
],
'arrow-function-returning-nowdoc' => [
'/* testNowdoc */',
[
'is' => true,
'get' => [
'parenthesis_opener' => 1,
'parenthesis_closer' => 2,
'scope_opener' => 4,
'scope_closer' => 9,
],
],
],
];
}
}
10 changes: 10 additions & 0 deletions Tests/Utils/FunctionDeclarations/IsArrowFunctionTest.inc
Expand Up @@ -88,6 +88,16 @@ array_map(
/* testTernary */
$fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b';

$foo = foo(
/* testArrowFunctionAsArgument */
fn() => bar()
);

$foo = foo(
/* testArrowFunctionWithArrayAsArgument */
fn() => [$row[0], $row[3]]
);

/* testConstantDeclaration */
const FN = 'a';

Expand Down
24 changes: 24 additions & 0 deletions Tests/Utils/FunctionDeclarations/IsArrowFunctionTest.php
Expand Up @@ -456,6 +456,30 @@ public function dataArrowFunction()
],
],
],
'arrow-function-as-function-call-argument' => [
'/* testArrowFunctionAsArgument */',
[
'is' => true,
'get' => [
'parenthesis_opener' => 1,
'parenthesis_closer' => 2,
'scope_opener' => 4,
'scope_closer' => 8,
],
],
],
'arrow-function-as-function-call-argument-with-array-return' => [
'/* testArrowFunctionWithArrayAsArgument */',
[
'is' => true,
'get' => [
'parenthesis_opener' => 1,
'parenthesis_closer' => 2,
'scope_opener' => 4,
'scope_closer' => 17,
],
],
],
'arrow-function-nested-in-method' => [
'/* testNestedInMethod */',
[
Expand Down

0 comments on commit b5b1c57

Please sign in to comment.