Skip to content

Commit

Permalink
✨ New PHPCSUtils\Utils\Constants class
Browse files Browse the repository at this point in the history
... to contain utilities methods for analysing constants declared using the `const` keyword.

In the future, the scope of the methods might be expanded to also covered constants declared using `define()`, but that's for later.

Initially, the class comes with the following method:
* `getProperties(File $phpcsFile, $stackPtr): array` to retrieve an array of information about a constant declaration, like the visibility, whether visibility was explicitly declared, whether the constant was declared as final, what the type is for the constant etc.

Includes extensive unit tests.
  • Loading branch information
jrfnl committed May 10, 2024
1 parent 18155c7 commit 114217e
Show file tree
Hide file tree
Showing 9 changed files with 1,285 additions and 0 deletions.
180 changes: 180 additions & 0 deletions PHPCSUtils/Utils/Constants.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?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\Utils;

use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Internal\Cache;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Scopes;

/**
* Utility functions for use when examining constants declared using the "const" keyword.
*
* @since 1.1.0
*/
final class Constants
{

/**
* Retrieve the visibility and implementation properties of an OO constant.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position in the stack of the `T_CONST` token
* to acquire the properties for.
*
* @return array<string, string|int|bool> Array with information about the constant declaration.
* The format of the return value is:
* ```php
* array(
* 'scope' => string, // Public, private, or protected.
* 'scope_token' => integer|false, // The stack pointer to the scope keyword or
* // FALSE if the scope was not explicitly specified.
* 'is_final' => boolean, // TRUE if the final keyword was found.
* 'final_token' => integer|false, // The stack pointer to the final keyword
* // or FALSE if the const is not declared final.
* 'type' => string, // The type of the const (empty if no type specified).
* 'type_token' => integer|false, // The stack pointer to the start of the type
* // or FALSE if there is no type.
* 'type_end_token' => integer|false, // The stack pointer to the end of the type
* // or FALSE if there is no type.
* 'nullable_type' => boolean, // TRUE if the type is preceded by the
* // nullability operator.
* 'name_token' => integer, // The stack pointer to the constant name.
* // Note: for group declarations this points to the
* // name of the first constant.
* 'equal_token' => integer, // The stack pointer to the equal sign.
* // Note: for group declarations this points to the
* // equal sign of the first constant.
* );
* ```
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a `T_CONST` token.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not an OO constant.
*/
public static function getProperties(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CONST) {
throw new RuntimeException('$stackPtr must be of type T_CONST');
}

if (Scopes::isOOConstant($phpcsFile, $stackPtr) === false) {
throw new RuntimeException('$stackPtr is not an OO constant');
}

if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) {
return Cache::get($phpcsFile, __METHOD__, $stackPtr);
}

$assignmentPtr = $phpcsFile->findNext([\T_EQUAL, \T_SEMICOLON, \T_CLOSE_CURLY_BRACKET], ($stackPtr + 1));
if ($assignmentPtr === false || $tokens[$assignmentPtr]['code'] !== \T_EQUAL) {
// Probably a parse error. Don't cache the result.
throw new RuntimeException('$stackPtr is not an OO constant');
}

$namePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($assignmentPtr - 1), ($stackPtr + 1), true);

$returnValue = [
'scope' => 'public',
'scope_token' => false,
'is_final' => false,
'final_token' => false,
'type' => '',
'type_token' => false,
'type_end_token' => false,
'nullable_type' => false,
'name_token' => $namePtr,
'equal_token' => $assignmentPtr,
];

for ($i = ($stackPtr - 1);; $i--) {
// Skip over potentially large docblocks.
if ($tokens[$i]['code'] === \T_DOC_COMMENT_CLOSE_TAG
&& isset($tokens[$i]['comment_opener'])
) {
$i = $tokens[$i]['comment_opener'];
continue;
}

if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
continue;
}

switch ($tokens[$i]['code']) {
case \T_PUBLIC:
$returnValue['scope'] = 'public';
$returnValue['scope_token'] = $i;
break;

case \T_PROTECTED:
$returnValue['scope'] = 'protected';
$returnValue['scope_token'] = $i;
break;

case \T_PRIVATE:
$returnValue['scope'] = 'private';
$returnValue['scope_token'] = $i;
break;

case \T_FINAL:
$returnValue['is_final'] = true;
$returnValue['final_token'] = $i;
break;

default:
// Any other token means that the start of the statement has been reached.
break 2;
}
}

$type = '';
$typeToken = false;
$typeEndToken = false;
$constantTypeTokens = Collections::constantTypeTokens();

// Now, let's check for a type.
for ($i = ($stackPtr + 1); $i < $namePtr; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
continue;
}

if ($tokens[$i]['code'] === \T_NULLABLE) {
$returnValue['nullable_type'] = true;
continue;
}

if (isset($constantTypeTokens[$tokens[$i]['code']]) === true) {
$typeEndToken = $i;
if ($typeToken === false) {
$typeToken = $i;
}

$type .= $tokens[$i]['content'];
}
}

if ($type !== '' && $returnValue['nullable_type'] === true) {
$type = '?' . $type;
}

$returnValue['type'] = $type;
$returnValue['type_token'] = $typeToken;
$returnValue['type_end_token'] = $typeEndToken;

Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue);
return $returnValue;
}
}
6 changes: 6 additions & 0 deletions Tests/Utils/Constants/GetPropertiesParseError1Test.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

class LiveCodingParseError {
/* testParseErrorLiveCoding */
final const false
}
40 changes: 40 additions & 0 deletions Tests/Utils/Constants/GetPropertiesParseError1Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2024 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Tests\Utils\Constants;

use PHPCSUtils\TestUtils\UtilityMethodTestCase;
use PHPCSUtils\Utils\Constants;

/**
* Tests for the \PHPCSUtils\Utils\Constants::getProperties method.
*
* @covers \PHPCSUtils\Utils\Constants::getProperties
*
* @group constants
*
* @since 1.1.0
*/
final class GetPropertiesParseError1Test extends UtilityMethodTestCase
{

/**
* Test receiving an exception when encountering a specific parse error.
*
* @return void
*/
public function testParseError()
{
$this->expectPhpcsException('$stackPtr is not an OO constant');

$const = $this->getTargetToken('/* testParseErrorLiveCoding */', \T_CONST);
Constants::getProperties(self::$phpcsFile, $const);
}
}
5 changes: 5 additions & 0 deletions Tests/Utils/Constants/GetPropertiesParseError2Test.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

class LiveCodingParseError {
/* testParseErrorLiveCoding */
private const ?int TYPED_INT = 0;
40 changes: 40 additions & 0 deletions Tests/Utils/Constants/GetPropertiesParseError2Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2024 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Tests\Utils\Constants;

use PHPCSUtils\TestUtils\UtilityMethodTestCase;
use PHPCSUtils\Utils\Constants;

/**
* Tests for the \PHPCSUtils\Utils\Constants::getProperties method.
*
* @covers \PHPCSUtils\Utils\Constants::getProperties
*
* @group constants
*
* @since 1.1.0
*/
final class GetPropertiesParseError2Test extends UtilityMethodTestCase
{

/**
* Test receiving an exception when encountering a specific parse error.
*
* @return void
*/
public function testParseError()
{
$this->expectPhpcsException('$stackPtr is not an OO constant');

$const = $this->getTargetToken('/* testParseErrorLiveCoding */', \T_CONST);
Constants::getProperties(self::$phpcsFile, $const);
}
}
6 changes: 6 additions & 0 deletions Tests/Utils/Constants/GetPropertiesParseError3Test.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

class LiveCodingParseError {
/* testParseErrorMissingName */
private const = 0;
}
52 changes: 52 additions & 0 deletions Tests/Utils/Constants/GetPropertiesParseError3Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2024 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Tests\Utils\Constants;

use PHPCSUtils\TestUtils\UtilityMethodTestCase;
use PHPCSUtils\Utils\Constants;

/**
* Tests for the \PHPCSUtils\Utils\Constants::getProperties method.
*
* @covers \PHPCSUtils\Utils\Constants::getProperties
*
* @group constants
*
* @since 1.1.0
*/
final class GetPropertiesParseError3Test extends UtilityMethodTestCase
{

/**
* Test the getProperties() method returns false for the name pointer, when the name is missing.
*
* @return void
*/
public function testGetProperties()
{
$const = $this->getTargetToken('/* testParseErrorMissingName */', \T_CONST);
$expected = [
'scope' => 'private',
'scope_token' => ($const - 2),
'is_final' => false,
'final_token' => false,
'type' => '',
'type_token' => false,
'type_end_token' => false,
'nullable_type' => false,
'name_token' => false,
'equal_token' => ($const + 2),
];
$result = Constants::getProperties(self::$phpcsFile, $const);

$this->assertSame($expected, $result);
}
}
Loading

0 comments on commit 114217e

Please sign in to comment.