Skip to content

Commit

Permalink
✨ New ObjectDeclarations::getDeclared*() utility methods
Browse files Browse the repository at this point in the history
This commit adds a new set of utility methods to the `ObjectDeclarations` class:
* `getDeclaredConstants(File $phpcsFile, int $stackPtr): array`
* `getDeclaredEnumCases(File $phpcsFile, int $stackPtr): array`
* `getDeclaredProperties(File $phpcsFile, int $stackPtr): array`
* `getDeclaredMethods(File $phpcsFile, int $stackPtr): array`
* (`private`) `analyzeOOStructure(File $phpcsFile, int $stackPtr): array`

These methods allow for retrieving an array with the names of all constants, enum cases, properties and methods as the keys and the stack pointer to the relevant `T_CONST`, `T_ENUM_CASE`, `T_VARIABLE` or `T_FUNCTION` token as the value.

As these methods all used the same `analyzeOOStructure()` method under the hood and the results of that method are cached, the method are highly optimized for performance.

If a sniff needs to search for a named constant/enum case/property/method in an OO structure, in most cases, these methods should be the recommended way for finding the declaration, instead of the sniff attempting to do this itself.

Includes extensive unit tests.
  • Loading branch information
jrfnl committed May 10, 2024
1 parent 567d785 commit 88735be
Show file tree
Hide file tree
Showing 21 changed files with 1,828 additions and 0 deletions.
240 changes: 240 additions & 0 deletions PHPCSUtils/Utils/ObjectDeclarations.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
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\FunctionDeclarations;
use PHPCSUtils\Utils\GetTokensAsString;

/**
Expand Down Expand Up @@ -356,4 +358,242 @@ private static function findNames(File $phpcsFile, $stackPtr, $keyword, array $a

return $names;
}

/**
* Retrieve all constants declared in an OO structure.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The stack position of the OO keyword.
*
* @return array<string, int> Array with names of the found constants as keys and the stack pointers
* to the T_CONST token for each constant as values.
* If no constants are found or a parse error is encountered,
* an empty array is returned.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or
* is not an OO keyword token.
*/
public static function getDeclaredConstants(File $phpcsFile, $stackPtr)
{
return self::analyzeOOStructure($phpcsFile, $stackPtr)['constants'];
}

/**
* Retrieve all cases declared in an enum.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The stack position of the OO keyword.
*
* @return array<string, int> Array with names of the found cases as keys and the stack pointers
* to the T_ENUM_CASE token for each case as values.
* If no cases are found or a parse error is encountered,
* an empty array is returned.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or
* is not a T_ENUM token.
*/
public static function getDeclaredEnumCases(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_ENUM) {
throw new RuntimeException('$stackPtr must be of type T_ENUM');
}

return self::analyzeOOStructure($phpcsFile, $stackPtr)['cases'];
}

/**
* Retrieve all properties declared in an OO structure.
*
* Note: interfaces and enums cannot contain properties. This method does not take this into
* account to allow sniffs to flag this kind of incorrect PHP code.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The stack position of the OO keyword.
*
* @return array<string, int> Array with names of the found properties as keys and the stack pointers
* to the T_VARIABLE token for each property as values.
* If no properties are found or a parse error is encountered,
* an empty array is returned.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or
* is not an OO keyword token.
*/
public static function getDeclaredProperties(File $phpcsFile, $stackPtr)
{
return self::analyzeOOStructure($phpcsFile, $stackPtr)['properties'];
}

/**
* Retrieve all methods declared in an OO structure.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The stack pointer to the OO keyword.
*
* @return array<string, int> Array with names of the found methods as keys and the stack pointers
* to the T_FUNCTION keyword for each method as values.
* If no methods are found or a parse error is encountered,
* an empty array is returned.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or
* is not an OO keyword token.
*/
public static function getDeclaredMethods(File $phpcsFile, $stackPtr)
{
return self::analyzeOOStructure($phpcsFile, $stackPtr)['methods'];
}

/**
* Retrieve all constants, cases, properties and methods in an OO structure.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The stack position of the OO keyword.
*
* @return array<string, array<string, int>> Multi-dimensional array with four keys:
* - "constants"
* - "cases"
* - "properties"
* - "methods"
* Each index holds an associative array with the name of the "thing"
* as the key and the stack pointer to the related token as the value.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token doesn't exist or
* is not an OO keyword token.
*/
private static function analyzeOOStructure(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

if (isset($tokens[$stackPtr]) === false
|| isset(Tokens::$ooScopeTokens[$tokens[$stackPtr]['code']]) === false
) {
throw new RuntimeException(
'$stackPtr must be of type T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'
);
}

// Set defaults.
$found = [
'constants' => [],
'cases' => [],
'properties' => [],
'methods' => [],
];

if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
return $found;
}

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

for ($i = ($tokens[$stackPtr]['scope_opener'] + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) {
// Skip over potentially large docblocks.
if (isset($tokens[$i]['comment_closer']) === true) {
$i = $tokens[$i]['comment_closer'];
continue;
}

// Skip over attributes.
if (isset($tokens[$i]['attribute_closer']) === true) {
$i = $tokens[$i]['attribute_closer'];
continue;
}

// Skip over trait imports with conflict resolution.
if ($tokens[$i]['code'] === \T_USE
&& isset($tokens[$i]['scope_closer']) === true
) {
$i = $tokens[$i]['scope_closer'];
continue;
}

// Defensive coding against parse errors.
if ($tokens[$i]['code'] === \T_CLOSURE
&& isset($tokens[$i]['scope_closer']) === true
) {
$i = $tokens[$i]['scope_closer'];
continue;
}

switch ($tokens[$i]['code']) {
case \T_CONST:
$assignmentPtr = $phpcsFile->findNext([\T_EQUAL, \T_SEMICOLON, \T_CLOSE_CURLY_BRACKET], ($i + 1));
if ($assignmentPtr === false || $tokens[$assignmentPtr]['code'] !== \T_EQUAL) {
// Probably a parse error. Ignore.
continue 2;
}

$namePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($assignmentPtr - 1), ($i + 1), true);
if ($namePtr === false || $tokens[$namePtr]['code'] !== \T_STRING) {
// Probably a parse error. Ignore.
continue 2;
}

$found['constants'][$tokens[$namePtr]['content']] = $i;

// Skip to the assignment pointer, no need to double walk.
$i = $assignmentPtr;
break;

case \T_ENUM_CASE:
$namePtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
if ($namePtr === false || $tokens[$namePtr]['code'] !== \T_STRING) {
// Probably a parse error. Ignore.
continue 2;
}

$name = $tokens[$namePtr]['content'];
$found['cases'][$name] = $i;

// Skip to the name pointer, no need to double walk.
$i = $namePtr;
break;

case \T_VARIABLE:
$name = $tokens[$i]['content'];
$found['properties'][$name] = $i;
break;

case \T_FUNCTION:
$name = self::getName($phpcsFile, $i);
if (\is_string($name) && $name !== '') {
$found['methods'][$name] = $i;

if (\strtolower($name) === '__construct') {
// Check for constructor property promotion.
$parameters = FunctionDeclarations::getParameters($phpcsFile, $i);
foreach ($parameters as $param) {
if (isset($param['property_visibility'])) {
$found['properties'][$param['name']] = $param['token'];
}
}
}
}

if (isset($tokens[$i]['scope_closer']) === true) {
// Skip over the contents of the method, including the parameters.
$i = $tokens[$i]['scope_closer'];
} elseif (isset($tokens[$i]['parenthesis_closer']) === true) {
// Skip over the contents of an abstract/interface method, including the parameters.
$i = $tokens[$i]['parenthesis_closer'];
}
break;
}
}

Cache::set($phpcsFile, __METHOD__, $stackPtr, $found);
return $found;
}
}
Loading

0 comments on commit 88735be

Please sign in to comment.