diff --git a/PHPCSUtils/Utils/Parentheses.php b/PHPCSUtils/Utils/Parentheses.php index 507e0eb8..c26734fc 100644 --- a/PHPCSUtils/Utils/Parentheses.php +++ b/PHPCSUtils/Utils/Parentheses.php @@ -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 => + */ + 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. * @@ -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) @@ -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) ) { diff --git a/Tests/Utils/Parentheses/ParenthesesTest.inc b/Tests/Utils/Parentheses/ParenthesesTest.inc index 545f8aa5..60d5599e 100644 --- a/Tests/Utils/Parentheses/ParenthesesTest.inc +++ b/Tests/Utils/Parentheses/ParenthesesTest.inc @@ -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 diff --git a/Tests/Utils/Parentheses/ParenthesesTest.php b/Tests/Utils/Parentheses/ParenthesesTest.php index cf1a7a34..5c4f0cfc 100644 --- a/Tests/Utils/Parentheses/ParenthesesTest.php +++ b/Tests/Utils/Parentheses/ParenthesesTest.php @@ -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, @@ -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. * @@ -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, ]; /** @@ -348,7 +388,7 @@ public function testPassingParenthesisTokenToMethodsWhichExpectParenthesisClose( * * @return void */ - public function testPassingParenthesisCloseHandlinginBCLayer() + public function testPassingParenthesisCloseHandlingInBCLayer() { $stackPtr = $this->getTargetToken('/* testListOnCloseParens */', \T_CLOSE_PARENTHESIS); @@ -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', [ @@ -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' => [ @@ -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, + ], ]; } }