Skip to content

Commit

Permalink
Merge pull request #176 from PHPCSStandards/feature/php8-nullsafe-ope…
Browse files Browse the repository at this point in the history
…rator

PHP 8.0 | Add utilities to handle the nullsafe object operator
  • Loading branch information
jrfnl committed Aug 7, 2020
2 parents fc49241 + 0ec9ce6 commit 52530d7
Show file tree
Hide file tree
Showing 10 changed files with 513 additions and 2 deletions.
118 changes: 117 additions & 1 deletion PHPCSUtils/Tokens/Collections.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,13 @@ class Collections
];

/**
* Object operators.
* DEPRECATED: Object operators.
*
* @since 1.0.0-alpha3
*
* @deprecated 1.0.0-alpha4 Use the {@see \PHPCSUtils\Tokens\Collections::objectOperators()}
* method instead.
*
* @var array <int> => <int>
*/
public static $objectOperators = [
Expand Down Expand Up @@ -576,6 +579,119 @@ public static function functionDeclarationTokensBC()
return $tokens;
}

/**
* Object operators.
*
* Note: this is a method, not a property as the `T_NULLSAFE_OBJECT_OPERATOR` token may not exist.
*
* Sister-method to the {@see Collections::objectOperatorsBC()} method.
* This method supports PHP 8.0 and up.
* The {@see Collections::objectOperatorsBC()} method supports PHP < 8.0.
*
* This method can also safely be used if the token collection is only used when looking back
* via `$phpcsFile->findPrevious()` as in that case, a non-backfilled nullsafe object operator
* will still match the "normal" object operator.
*
* @see \PHPCSUtils\Tokens\Collections::objectOperatorsBC() Related method (PHP < 8.0).
*
* @since 1.0.0-alpha4
*
* @return array <int|string> => <int|string>
*/
public static function objectOperators()
{
$tokens = [
\T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR,
\T_DOUBLE_COLON => \T_DOUBLE_COLON,
];

if (\defined('T_NULLSAFE_OBJECT_OPERATOR') === true) {
// PHP 8.0.
$tokens[\T_NULLSAFE_OBJECT_OPERATOR] = \T_NULLSAFE_OBJECT_OPERATOR;
}

return $tokens;
}

/**
* Object operators.
*
* Note: this is a method, not a property as the `T_NULLSAFE_OBJECT_OPERATOR` token may not exist.
*
* Sister-method to the {@see Collections::objectOperators()} method.
* The {@see Collections::objectOperators()} method supports PHP 8.0 and up.
* This method supports PHP < 8.0.
*
* Notable difference:
* - This method accounts for tokens which may be encountered when the `T_NULLSAFE_OBJECT_OPERATOR` token
* doesn't exist.
*
* It is recommended to use the {@see Collections::objectOperators()} method instead of
* this method if a standard does not need to support PHP < 8.0.
*
* The {@see Collections::objectOperators()} method can also safely be used if the token collection
* is only used when looking back via `$phpcsFile->findPrevious()` as in that case, a non-backfilled
* nullsafe object operator will still match the "normal" object operator.
*
* Note: if this method is used, the {@see \PHPCSUtils\Utils\Operators::isNullsafeObjectOperator()}
* method needs to be used on potential nullsafe object operator tokens to verify whether it really
* is a nullsafe object operator or not.
*
* @see \PHPCSUtils\Tokens\Collections::objectOperators() Related method (PHP 8.0+).
* @see \PHPCSUtils\Tokens\Collections::nullsafeObjectOperatorBC() Tokens which can represent a
* nullsafe object operator.
* @see \PHPCSUtils\Utils\Operators::isNullsafeObjectOperator() Nullsafe object operator detection for
* PHP < 8.0.
*
* @since 1.0.0-alpha4
*
* @return array <int|string> => <int|string>
*/
public static function objectOperatorsBC()
{
$tokens = [
\T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR,
\T_DOUBLE_COLON => \T_DOUBLE_COLON,
];

$tokens += self::nullsafeObjectOperatorBC();

return $tokens;
}

/**
* Tokens which can represent the nullsafe object operator.
*
* This method will return the appropriate tokens based on the PHP/PHPCS version used.
*
* Note: this is a method, not a property as the `T_NULLSAFE_OBJECT_OPERATOR` token may not exist.
*
* Note: if this method is used, the {@see \PHPCSUtils\Utils\Operators::isNullsafeObjectOperator()}
* method needs to be used on potential nullsafe object operator tokens to verify whether it really
* is a nullsafe object operator or not.
*
* @see \PHPCSUtils\Utils\Operators::isNullsafeObjectOperator() Nullsafe object operator detection for
* PHP < 8.0.
*
* @since 1.0.0-alpha4
*
* @return array <int|string> => <int|string>
*/
public static function nullsafeObjectOperatorBC()
{
if (\defined('T_NULLSAFE_OBJECT_OPERATOR') === true) {
// PHP 8.0.
return [
\T_NULLSAFE_OBJECT_OPERATOR => \T_NULLSAFE_OBJECT_OPERATOR,
];
}

return [
\T_INLINE_THEN => \T_INLINE_THEN,
\T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR,
];
}

/**
* Token types which can be encountered in a parameter type declaration.
*
Expand Down
2 changes: 1 addition & 1 deletion PHPCSUtils/Utils/Namespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static function getType(File $phpcsFile, $stackPtr)
+ Tokens::$castTokens
+ Tokens::$blockOpeners
+ Collections::$incrementDecrementOperators
+ Collections::$objectOperators;
+ Collections::objectOperators();

$findAfter[\T_OPEN_CURLY_BRACKET] = \T_OPEN_CURLY_BRACKET;
$findAfter[\T_OPEN_SQUARE_BRACKET] = \T_OPEN_SQUARE_BRACKET;
Expand Down
50 changes: 50 additions & 0 deletions PHPCSUtils/Utils/Operators.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,56 @@ public static function isTypeUnion(File $phpcsFile, $stackPtr)
return false;
}

/**
* Determine whether a token is (part of) a nullsafe object operator.
*
* Helper method for PHP < 8.0 in combination with PHPCS versions in which the
* `T_NULLSAFE_OBJECT_OPERATOR` token is not yet backfilled.
*
* @since 1.0.0-alpha4
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the T_INLINE_THEN or T_OBJECT_OPERATOR
* token in the stack.
*
* @return bool `TRUE` if nullsafe object operator; or `FALSE` otherwise.
*/
public static function isNullsafeObjectOperator(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]) === false) {
return false;
}

// Safeguard in case this method is used on PHP 8 and the nullsafe object operator would be passed.
if ($tokens[$stackPtr]['type'] === 'T_NULLSAFE_OBJECT_OPERATOR') {
return true;
}

if (isset(Collections::nullsafeObjectOperatorBC()[$tokens[$stackPtr]['code']]) === false) {
return false;
}

/*
* Note: not bypassing empty tokens as whitespace and comments are not allowed
* within an operator.
*/
if ($tokens[$stackPtr]['code'] === \T_INLINE_THEN) {
if (isset($tokens[$stackPtr + 1]) && $tokens[$stackPtr + 1]['code'] === \T_OBJECT_OPERATOR) {
return true;
}
}

if ($tokens[$stackPtr]['code'] === \T_OBJECT_OPERATOR) {
if (isset($tokens[$stackPtr - 1]) && $tokens[$stackPtr - 1]['code'] === \T_INLINE_THEN) {
return true;
}
}

// Not a nullsafe object operator token.
return false;
}

/**
* Determine whether a T_MINUS/T_PLUS token is a unary operator.
*
Expand Down
50 changes: 50 additions & 0 deletions Tests/Tokens/Collections/NullsafeObjectOperatorTokensBCTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?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\Tokens\Collections;

use PHPCSUtils\Tokens\Collections;
use PHPUnit\Framework\TestCase;

/**
* Test class.
*
* @covers \PHPCSUtils\Tokens\Collections::nullsafeObjectOperatorBC
*
* @group collections
*
* @since 1.0.0
*/
class NullsafeObjectOperatorBCTest extends TestCase
{

/**
* Test the method.
*
* @return void
*/
public function testNullsafeObjectOperatorBC()
{
$expected = [];
if (\version_compare(\PHP_VERSION_ID, '80000', '>=') === true
) {
$expected = [
\T_NULLSAFE_OBJECT_OPERATOR => \T_NULLSAFE_OBJECT_OPERATOR,
];
} else {
$expected = [
\T_INLINE_THEN => \T_INLINE_THEN,
\T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR,
];
}

$this->assertSame($expected, Collections::nullsafeObjectOperatorBC());
}
}
49 changes: 49 additions & 0 deletions Tests/Tokens/Collections/ObjectOperatorsBCTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?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\Tokens\Collections;

use PHPCSUtils\Tokens\Collections;
use PHPUnit\Framework\TestCase;

/**
* Test class.
*
* @covers \PHPCSUtils\Tokens\Collections::objectOperatorsBC
*
* @group collections
*
* @since 1.0.0
*/
class ObjectOperatorsBCTest extends TestCase
{

/**
* Test the method.
*
* @return void
*/
public function testObjectOperatorsBC()
{
$expected = [
\T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR,
\T_DOUBLE_COLON => \T_DOUBLE_COLON,
];

if (\version_compare(\PHP_VERSION_ID, '80000', '>=') === true
) {
$expected[\T_NULLSAFE_OBJECT_OPERATOR] = \T_NULLSAFE_OBJECT_OPERATOR;
} else {
$expected[\T_INLINE_THEN] = \T_INLINE_THEN;
}

$this->assertSame($expected, Collections::objectOperatorsBC());
}
}
47 changes: 47 additions & 0 deletions Tests/Tokens/Collections/ObjectOperatorsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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\Tokens\Collections;

use PHPCSUtils\Tokens\Collections;
use PHPUnit\Framework\TestCase;

/**
* Test class.
*
* @covers \PHPCSUtils\Tokens\Collections::objectOperators
*
* @group collections
*
* @since 1.0.0
*/
class ObjectOperatorsTest extends TestCase
{

/**
* Test the method.
*
* @return void
*/
public function testObjectOperators()
{
$expected = [
\T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR,
\T_DOUBLE_COLON => \T_DOUBLE_COLON,
];

if (\version_compare(\PHP_VERSION_ID, '80000', '>=') === true
) {
$expected[\T_NULLSAFE_OBJECT_OPERATOR] = \T_NULLSAFE_OBJECT_OPERATOR;
}

$this->assertSame($expected, Collections::objectOperators());
}
}
3 changes: 3 additions & 0 deletions Tests/Utils/Namespaces/NamespaceTypeTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ namespace\ClassName::$property++;
/* testNamespaceOperatorGlobalNamespaceStartOfStatementCombiWithNonConfusingToken3 */
namespace\CONSTANT['key'];

/* testNamespaceOperatorGlobalNamespaceStartOfStatementCombiWithNonConfusingToken4 */
namespace\functionReturningObj()?->chained();


/* testParseErrorScopedNamespaceDeclaration */
function testScope() {
Expand Down
7 changes: 7 additions & 0 deletions Tests/Utils/Namespaces/NamespaceTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ public function dataNamespaceType()
'operator' => true,
],
],
'namespace-operator-global-namespace-start-of-statement-with-non-confusing-token-4' => [
'/* testNamespaceOperatorGlobalNamespaceStartOfStatementCombiWithNonConfusingToken4 */',
[
'declaration' => false,
'operator' => true,
],
],
'parse-error-scoped-namespace-declaration' => [
'/* testParseErrorScopedNamespaceDeclaration */',
[
Expand Down
Loading

0 comments on commit 52530d7

Please sign in to comment.