Skip to content

Commit

Permalink
Merge e678aed into 5a2a3c9
Browse files Browse the repository at this point in the history
  • Loading branch information
jrfnl committed Jul 29, 2019
2 parents 5a2a3c9 + e678aed commit 7252823
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 22 deletions.
115 changes: 97 additions & 18 deletions PHPCompatibility/Sniffs/Syntax/NewFunctionArrayDereferencingSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
use PHP_CodeSniffer_Tokens as Tokens;

/**
* \PHPCompatibility\Sniffs\Syntax\NewFunctionArrayDereferencingSniff.
* PHP 5.4 supports direct array dereferencing on the return of a method/function call.
*
* As of PHP 7.0, this also works when using curly braces for the dereferencing.
* While unclear, this most likely has to do with the Uniform Variable Syntax changes.
*
* PHP version 5.4
* PHP version 7.0
*
* @category PHP
* @package PHPCompatibility
* @author Wim Godden <wim.godden@cu.be>
* @link https://wiki.php.net/rfc/functionarraydereferencing
* @link https://wiki.php.net/rfc/uniform_variable_syntax
*
* @internal The reason for splitting the logic of this sniff into different methods is
* to allow re-use of the logic by the PHP 7.4 RemovedCurlyBraceArrayAccess sniff.
*
* @since 9.3.0 Now also detects dereferencing using curly braces.
*/
class NewFunctionArrayDereferencingSniff extends Sniff
{
Expand All @@ -47,26 +55,76 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($this->supportsBelow('5.3') === false) {
if ($this->supportsBelow('5.6') === false) {
return;
}

$dereferencing = $this->isFunctionArrayDereferencing($phpcsFile, $stackPtr);
if (empty($dereferencing)) {
return;
}

$tokens = $phpcsFile->getTokens();
$supports53 = $this->supportsBelow('5.3');

foreach ($dereferencing as $openBrace => $closeBrace) {
if ($supports53 === true
&& $tokens[$openBrace]['type'] === 'T_OPEN_SQUARE_BRACKET'
) {
$phpcsFile->addError(
'Function array dereferencing is not present in PHP version 5.3 or earlier',
$openBrace,
'Found'
);

continue;
}

// PHP 7.0 function array dereferencing using curly braces.
if ($tokens[$openBrace]['type'] === 'T_OPEN_CURLY_BRACKET') {
$phpcsFile->addError(
'Function array dereferencing using curly braces is not present in PHP version 5.6 or earlier',
$openBrace,
'FoundUsingCurlies'
);
}
}
}


/**
* Check if the return of a function/method call is being dereferenced.
*
* @since 9.3.0 Logic split off from the process method.
*
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in
* the stack passed in $tokens.
*
* @return array Array containing stack pointers to the open/close braces
* involved in the function dereferencing;
* or an empty array if no function dereferencing was detected.
*/
public function isFunctionArrayDereferencing(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

// Next non-empty token should be the open parenthesis.
$openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
if ($openParenthesis === false || $tokens[$openParenthesis]['code'] !== \T_OPEN_PARENTHESIS) {
return;
return array();
}

// Don't throw errors during live coding.
if (isset($tokens[$openParenthesis]['parenthesis_closer']) === false) {
return;
return array();
}

// Is this T_STRING really a function or method call ?
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prevToken !== false && \in_array($tokens[$prevToken]['code'], array(\T_DOUBLE_COLON, \T_OBJECT_OPERATOR), true) === false) {
if ($prevToken !== false
&& \in_array($tokens[$prevToken]['code'], array(\T_DOUBLE_COLON, \T_OBJECT_OPERATOR), true) === false
) {
$ignore = array(
\T_FUNCTION => true,
\T_CONST => true,
Expand All @@ -78,18 +136,39 @@ public function process(File $phpcsFile, $stackPtr)

if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function or method.
return;
return array();
}
}

$closeParenthesis = $tokens[$openParenthesis]['parenthesis_closer'];
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenthesis + 1), null, true, null, true);
if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['type'] === 'T_OPEN_SQUARE_BRACKET') {
$phpcsFile->addError(
'Function array dereferencing is not present in PHP version 5.3 or earlier',
$nextNonEmpty,
'Found'
);
}
$current = $tokens[$openParenthesis]['parenthesis_closer'];
$braces = array();

do {
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), null, true, null, true);
if ($nextNonEmpty === false) {
break;
}

if ($tokens[$nextNonEmpty]['type'] === 'T_OPEN_SQUARE_BRACKET'
|| $tokens[$nextNonEmpty]['type'] === 'T_OPEN_CURLY_BRACKET' // PHP 7.0+.
) {
if (isset($tokens[$nextNonEmpty]['bracket_closer']) === false) {
// Live coding or parse error.
break;
}

$braces[$nextNonEmpty] = $tokens[$nextNonEmpty]['bracket_closer'];

// Continue, just in case there is nested array access, i.e. `echo $foo->bar()[0][2];`.
$current = $tokens[$nextNonEmpty]['bracket_closer'];
continue;
}

// If we're still here, we've reached the end of the function call.
break;

} while (true);

return $braces;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@ echo $foo->bar()[1];
echo $foo->bar()->baz()[2];
echo testClass::another_test()[0];

/*
* PHP 7.0: function array dereferencing using curly braces.
* This "silently" started working in PHP 7.0. See: https://3v4l.org/a1TW6
*/
echo test(){0};
echo $foo->bar(){1};
echo $foo->bar()->baz(){2};
echo testClass::another_test(){0};

// Mixing access with square brackets and curly braces.
echo $foo->bar()->baz()[0]{2}; // Should give two errors (depending on supported PHP version).
echo $foo->bar()->baz(){0}[2]; // Should give two errors (depending on supported PHP version).

// Don't throw errors during live code review.
echo test(
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,21 @@ class NewFunctionArrayDereferencingUnitTest extends BaseSniffTest
*
* @dataProvider dataArrayDereferencing
*
* @param int $line Line number with valid code.
* @param int $line The line number.
* @param bool $skipNoViolation Optional. Whether or not to test for no violation.
* Defaults to false.
*
* @return void
*/
public function testArrayDereferencing($line)
public function testArrayDereferencing($line, $skipNoViolation = false)
{
$file = $this->sniffFile(__FILE__, '5.3');
$this->assertError($file, $line, 'Function array dereferencing is not present in PHP version 5.3 or earlier');

if ($skipNoViolation === false) {
$file = $this->sniffFile(__FILE__, '5.4');
$this->assertNoViolation($file, $line);
}
}

/**
Expand All @@ -53,6 +60,43 @@ public function dataArrayDereferencing()
array(14),
array(15),
array(16),
array(28, true),
array(29, true),
);
}


/**
* testArrayDereferencingUsingCurlies
*
* @dataProvider dataArrayDereferencingUsingCurlies
*
* @param int $line Line number with valid code.
*
* @return void
*/
public function testArrayDereferencingUsingCurlies($line)
{
$file = $this->sniffFile(__FILE__, '5.6');
$this->assertError($file, $line, 'Function array dereferencing using curly braces is not present in PHP version 5.6 or earlier');
}

/**
* Data provider.
*
* @see testArrayDereferencingUsingCurlies()
*
* @return array
*/
public function dataArrayDereferencingUsingCurlies()
{
return array(
array(22),
array(23),
array(24),
array(25),
array(28),
array(29),
);
}

Expand Down Expand Up @@ -87,7 +131,7 @@ public function dataNoFalsePositives()
array(9),
array(10),
array(11),
array(19),
array(32),
);
}

Expand All @@ -99,7 +143,7 @@ public function dataNoFalsePositives()
*/
public function testNoViolationsInFileOnValidVersion()
{
$file = $this->sniffFile(__FILE__, '5.4');
$file = $this->sniffFile(__FILE__, '7.0');
$this->assertNoViolation($file);
}
}

0 comments on commit 7252823

Please sign in to comment.