Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parentheses: recognize more parentheses owners #215

Merged
merged 1 commit into from
Sep 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions PHPCSUtils/Utils/Parentheses.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,47 @@

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\BackCompat\Helper;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\FunctionDeclarations;

/**
* Utility functions for use when examining parenthesis tokens and arbitrary tokens wrapped in
* parentheses.
* Utility functions for use when examining parenthesis tokens and arbitrary tokens wrapped
* in parentheses.
*
* In contrast to PHPCS natively, `isset()`, `unset()`, `empty()`, `exit()`, `die()` and `eval()`
* will be considered parentheses owners by the functions in this class.
*
* @since 1.0.0
* @since 1.0.0-alpha4 Added support for `isset()`, `unset()`, `empty()`, `exit()`, `die()`
* and `eval()`` as parentheses owners to all applicable functions.
*/
class Parentheses
{

/**
* Extra tokens which should be considered parentheses owners.
*
* - `T_LIST` and `T_ANON_CLASS` only became parentheses owners in PHPCS 3.5.0.
* - `T_ISSET`, `T_UNSET`, `T_EMPTY`, `T_EXIT` and `T_EVAL` are not PHPCS native parentheses,
* owners, but are considered such for the purposes of this class.
* Also {@see https://github.com/squizlabs/PHP_CodeSniffer/issues/3118}.
*
* @since 1.0.0-alpha4
*
* @var array <int|string> => <int|string>
*/
private static $extraParenthesesOwners = [
\T_LIST => \T_LIST,
\T_ISSET => \T_ISSET,
\T_UNSET => \T_UNSET,
\T_EMPTY => \T_EMPTY,
\T_EXIT => \T_EXIT,
\T_EVAL => \T_EVAL,
\T_ANON_CLASS => \T_ANON_CLASS,
// Work-around: anon classes were, in certain circumstances, tokenized as T_CLASS prior to PHPCS 3.4.0.
\T_CLASS => \T_CLASS,
];

/**
* Get the pointer to the parentheses owner of an open/close parenthesis.
*
Expand All @@ -47,18 +75,13 @@ public static function getOwner(File $phpcsFile, $stackPtr)
}

/*
* `T_LIST` and `T_ANON_CLASS` only became parentheses owners in PHPCS 3.5.0.
* `T_FN` was only backfilled in PHPCS 3.5.3/4/5.
* - On PHP 7.4 with PHPCS < 3.5.3, T_FN will not yet be a parentheses owner.
* - On PHP < 7.4 with PHPCS < 3.5.3, T_FN will be tokenized as T_STRING and not yet be a parentheses owner.
* - `T_FN` was only backfilled in PHPCS 3.5.3/4/5.
* - On PHP 7.4 with PHPCS < 3.5.3, T_FN will not yet be a parentheses owner.
* - On PHP < 7.4 with PHPCS < 3.5.3, T_FN will be tokenized as T_STRING and not yet be a parentheses owner.
*
* {@internal As the 'parenthesis_owner' index is only set on parentheses, we didn't need to do any
* input validation before, but now we do.}
*/
if (\version_compare(Helper::getVersion(), '3.5.4', '>=') === true) {
return false;
}

if (isset($tokens[$stackPtr]) === false
|| ($tokens[$stackPtr]['code'] !== \T_OPEN_PARENTHESIS
&& $tokens[$stackPtr]['code'] !== \T_CLOSE_PARENTHESIS)
Expand All @@ -75,10 +98,7 @@ public static function getOwner(File $phpcsFile, $stackPtr)

$prevNonEmpty = $phpcsFile->findPrevious($skip, ($stackPtr - 1), null, true);
if ($prevNonEmpty !== false
&& ($tokens[$prevNonEmpty]['code'] === \T_LIST
|| $tokens[$prevNonEmpty]['code'] === \T_ANON_CLASS
// Work-around: anon classes were, in certain circumstances, tokenized as T_CLASS prior to PHPCS 3.4.0.
|| $tokens[$prevNonEmpty]['code'] === \T_CLASS
&& (isset(self::$extraParenthesesOwners[$tokens[$prevNonEmpty]['code']])
// Possibly an arrow function.
|| FunctionDeclarations::isArrowFunction($phpcsFile, $prevNonEmpty) === true)
) {
Expand Down
12 changes: 12 additions & 0 deletions Tests/Utils/Parentheses/ParenthesesTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ $value = $obj->fn(true);
/* testArrowFunctionByReference */
$fn = fn &($x) => $x;

/* testIfWithIssetEmpty */
if ( isset( $a, $b ) && ! empty ($c) ) {
/* testUnset */
unset($a[1], $b->prop);
}

/* testEval */
eval($a . $b . $c );

/* testExit */
if (defined('FOO') || die('message')) {}

// Intentional parse error. This has to be the last test in the file.
/* testParseError */
declare(ticks=1
210 changes: 207 additions & 3 deletions Tests/Utils/Parentheses/ParenthesesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,37 @@ class ParenthesesTest extends UtilityMethodTestCase
'code' => \T_VARIABLE,
'content' => '$x',
],
'testIfIsset-$b' => [
'marker' => '/* testIfWithIssetEmpty */',
'code' => \T_VARIABLE,
'content' => '$b',
],
'testIfEmpty-$c' => [
'marker' => '/* testIfWithIssetEmpty */',
'code' => \T_VARIABLE,
'content' => '$c',
],
'testUnset-->' => [
'marker' => '/* testUnset */',
'code' => \T_OBJECT_OPERATOR,
],
'testUnsetParenthesis' => [
'marker' => '/* testUnset */',
'code' => \T_OPEN_PARENTHESIS,
],
'testEval-concat' => [
'marker' => '/* testEval */',
'code' => \T_STRING_CONCAT,
],
'testIfExitDie-boolean-or' => [
'marker' => '/* testExit */',
'code' => \T_BOOLEAN_OR,
],
'testIfExitDie-message' => [
'marker' => '/* testExit */',
'code' => \T_CONSTANT_ENCAPSED_STRING,
'content' => "'message'",
],
'testParseError-1' => [
'marker' => '/* testParseError */',
'code' => \T_LNUMBER,
Expand All @@ -174,7 +205,9 @@ class ParenthesesTest extends UtilityMethodTestCase
* This array is merged with expected result arrays for various unit tests
* to make sure all possible parentheses owners are tested.
*
* This array should be kept in sync with the Tokens::$parenthesisOpeners array.
* This array should be kept in sync with the Tokens::$parenthesisOpeners array
* + the extra tokens the Parentheses class allows for.
*
* This array isn't auto-generated based on the array in Tokens as for these
* tests we want to have access to the token constant names, not just their values.
*
Expand All @@ -195,6 +228,13 @@ class ParenthesesTest extends UtilityMethodTestCase
'T_CATCH' => false,
'T_DECLARE' => false,
'T_FN' => false,

// Extra tokens.
'T_ISSET' => false,
'T_UNSET' => false,
'T_EMPTY' => false,
'T_EXIT' => false,
'T_EVAL' => false,
];

/**
Expand Down Expand Up @@ -348,7 +388,7 @@ public function testPassingParenthesisTokenToMethodsWhichExpectParenthesisClose(
*
* @return void
*/
public function testPassingParenthesisCloseHandlinginBCLayer()
public function testPassingParenthesisCloseHandlingInBCLayer()
{
$stackPtr = $this->getTargetToken('/* testListOnCloseParens */', \T_CLOSE_PARENTHESIS);

Expand Down Expand Up @@ -756,6 +796,125 @@ public function dataWalkParentheses()
'lastIfElseOwner' => false,
],
],
'testIfIsset-$b' => [
'testIfIsset-$b',
[
'firstOpener' => -8,
'firstCloser' => 14,
'firstOwner' => -10,
'firstScopeOwnerOpener' => -8,
'firstScopeOwnerCloser' => 14,
'firstScopeOwnerOwner' => -10,
'lastOpener' => -5,
'lastCloser' => 2,
'lastOwner' => -6,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => -10,
],
],
'testIfEmpty-$c' => [
'testIfEmpty-$c',
[
'firstOpener' => -19,
'firstCloser' => 3,
'firstOwner' => -21,
'firstScopeOwnerOpener' => -19,
'firstScopeOwnerCloser' => 3,
'firstScopeOwnerOwner' => -21,
'lastOpener' => -1,
'lastCloser' => 1,
'lastOwner' => -3,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => -21,
],
],
'testUnset-->' => [
'testUnset-->',
[
'firstOpener' => -8,
'firstCloser' => 2,
'firstOwner' => -9,
'firstScopeOwnerOpener' => false,
'firstScopeOwnerCloser' => false,
'firstScopeOwnerOwner' => false,
'lastOpener' => -8,
'lastCloser' => 2,
'lastOwner' => -9,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => false,
],
],
'testUnsetParenthesis' => [
'testUnsetParenthesis',
[
'firstOpener' => false,
'firstCloser' => false,
'firstOwner' => false,
'firstScopeOwnerOpener' => false,
'firstScopeOwnerCloser' => false,
'firstScopeOwnerOwner' => false,
'lastOpener' => false,
'lastCloser' => false,
'lastOwner' => false,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => false,
],
],
'testEval-concat' => [
'testEval-concat',
[
'firstOpener' => -3,
'firstCloser' => 8,
'firstOwner' => -4,
'firstScopeOwnerOpener' => false,
'firstScopeOwnerCloser' => false,
'firstScopeOwnerOwner' => false,
'lastOpener' => -3,
'lastCloser' => 8,
'lastOwner' => -4,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => false,
],
],
'testIfExitDie-boolean-or' => [
'testIfExitDie-boolean-or',
[
'firstOpener' => -6,
'firstCloser' => 6,
'firstOwner' => -8,
'firstScopeOwnerOpener' => -6,
'firstScopeOwnerCloser' => 6,
'firstScopeOwnerOwner' => -8,
'lastOpener' => -6,
'lastCloser' => 6,
'lastOwner' => -8,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => -8,
],
],
'testIfExitDie-message' => [
'testIfExitDie-message',
[
'firstOpener' => -10,
'firstCloser' => 2,
'firstOwner' => -12,
'firstScopeOwnerOpener' => -10,
'firstScopeOwnerCloser' => 2,
'firstScopeOwnerOwner' => -12,
'lastOpener' => -1,
'lastCloser' => 1,
'lastOwner' => -2,
'lastArrayOpener' => false,
'lastFunctionCloser' => false,
'lastIfElseOwner' => -12,
],
],
'testParseError-1' => [
'testParseError-1',
[
Expand Down Expand Up @@ -953,8 +1112,43 @@ public function dataHasOwner()
],
'testArrowFunctionReturnByRef' => [
'testArrowFunctionReturnByRef',
['T_FN' => true],
],
'testIfIsset-$b' => [
'testIfIsset-$b',
[
'T_IF' => true,
'T_ISSET' => true,
],
],
'testIfEmpty-$c' => [
'testIfEmpty-$c',
[
'T_IF' => true,
'T_EMPTY' => true,
],
],
'testUnset-->' => [
'testUnset-->',
['T_UNSET' => true],
],
'testUnsetParenthesis' => [
'testUnsetParenthesis',
[],
],
'testEval-concat' => [
'testEval-concat',
['T_EVAL' => true],
],
'testIfExitDie-boolean-or' => [
'testIfExitDie-boolean-or',
['T_IF' => true],
],
'testIfExitDie-message' => [
'testIfExitDie-message',
[
'T_FN' => true,
'T_IF' => true,
'T_EXIT' => true,
],
],
'testParseError-1' => [
Expand Down Expand Up @@ -1193,6 +1387,16 @@ public function dataLastOwnerIn()
$arrowFunctionOwners,
-4,
],
'testIfEmpty-$c-unset' => [
'testIfEmpty-$c',
[\T_UNSET],
false,
],
'testIfEmpty-$c-isset-empty' => [
'testIfEmpty-$c',
[\T_ISSET, \T_EMPTY],
-3,
],
];
}
}