360 lines
15 KiB
PHP
360 lines
15 KiB
PHP
|
<?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\Tokens\Collections;
|
||
|
use PHPCSUtils\Utils\GetTokensAsString;
|
||
|
|
||
|
/**
|
||
|
* Utility functions for use when examining object declaration statements.
|
||
|
*
|
||
|
* @since 1.0.0 The `ObjectDeclarations::get(Declaration)Name()`,
|
||
|
* `ObjectDeclarations::getClassProperties()`, `ObjectDeclarations::findExtendedClassName()`
|
||
|
* and `ObjectDeclarations::findImplementedInterfaceNames()` methods are based on and
|
||
|
* inspired by the methods of the same name in the PHPCS native
|
||
|
* PHP_CodeSniffer\Files\File` class.
|
||
|
* Also see {@see \PHPCSUtils\BackCompat\BCFile}.
|
||
|
*/
|
||
|
final class ObjectDeclarations
|
||
|
{
|
||
|
|
||
|
/**
|
||
|
* Retrieves the declaration name for classes, interfaces, traits, enums and functions.
|
||
|
*
|
||
|
* Main differences with the PHPCS version:
|
||
|
* - Defensive coding against incorrect calls to this method.
|
||
|
* - Improved handling of invalid names, like names starting with a number.
|
||
|
* This allows sniffs to report on invalid names instead of ignoring them.
|
||
|
* - Bug fix: improved handling of parse errors.
|
||
|
* Using the original method, a parse error due to an invalid name could cause the method
|
||
|
* to return the name of the *next* construct, a partial name and/or the name of a class
|
||
|
* being extended/interface being implemented.
|
||
|
* Using this version of the utility method, either the complete name (invalid or not) will
|
||
|
* be returned or `null` in case of no name (parse error).
|
||
|
*
|
||
|
* @see \PHP_CodeSniffer\Files\File::getDeclarationName() Original source.
|
||
|
* @see \PHPCSUtils\BackCompat\BCFile::getDeclarationName() Cross-version compatible version of the original.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
||
|
* @param int $stackPtr The position of the declaration token
|
||
|
* which declared the class, interface,
|
||
|
* trait, enum or function.
|
||
|
*
|
||
|
* @return string|null The name of the class, interface, trait, enum, or function;
|
||
|
* or `NULL` if the passed token doesn't exist, the function or
|
||
|
* class is anonymous or in case of a parse error/live coding.
|
||
|
*
|
||
|
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
|
||
|
* `T_FUNCTION`, `T_CLASS`, `T_ANON_CLASS`,
|
||
|
* `T_CLOSURE`, `T_TRAIT`, `T_ENUM` or `T_INTERFACE`.
|
||
|
*/
|
||
|
public static function getName(File $phpcsFile, $stackPtr)
|
||
|
{
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
|
||
|
if (isset($tokens[$stackPtr]) === false
|
||
|
|| ($tokens[$stackPtr]['code'] === \T_ANON_CLASS || $tokens[$stackPtr]['code'] === \T_CLOSURE)
|
||
|
) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$tokenCode = $tokens[$stackPtr]['code'];
|
||
|
|
||
|
if ($tokenCode !== \T_FUNCTION
|
||
|
&& $tokenCode !== \T_CLASS
|
||
|
&& $tokenCode !== \T_INTERFACE
|
||
|
&& $tokenCode !== \T_TRAIT
|
||
|
&& $tokenCode !== \T_ENUM
|
||
|
) {
|
||
|
throw new RuntimeException(
|
||
|
'Token type "' . $tokens[$stackPtr]['type']
|
||
|
. '" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if ($tokenCode === \T_FUNCTION
|
||
|
&& \strtolower($tokens[$stackPtr]['content']) !== 'function'
|
||
|
) {
|
||
|
// This is a function declared without the "function" keyword.
|
||
|
// So this token is the function name.
|
||
|
return $tokens[$stackPtr]['content'];
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Determine the name. Note that we cannot simply look for the first T_STRING
|
||
|
* because an (invalid) class name starting with a number will be multiple tokens.
|
||
|
* Whitespace or comment are however not allowed within a name.
|
||
|
*/
|
||
|
|
||
|
$stopPoint = $phpcsFile->numTokens;
|
||
|
if ($tokenCode === \T_FUNCTION && isset($tokens[$stackPtr]['parenthesis_opener']) === true) {
|
||
|
$stopPoint = $tokens[$stackPtr]['parenthesis_opener'];
|
||
|
} elseif (isset($tokens[$stackPtr]['scope_opener']) === true) {
|
||
|
$stopPoint = $tokens[$stackPtr]['scope_opener'];
|
||
|
}
|
||
|
|
||
|
$exclude = Tokens::$emptyTokens;
|
||
|
$exclude[] = \T_OPEN_PARENTHESIS;
|
||
|
$exclude[] = \T_OPEN_CURLY_BRACKET;
|
||
|
$exclude[] = \T_BITWISE_AND;
|
||
|
$exclude[] = \T_COLON; // Backed enums.
|
||
|
|
||
|
$nameStart = $phpcsFile->findNext($exclude, ($stackPtr + 1), $stopPoint, true);
|
||
|
if ($nameStart === false) {
|
||
|
// Live coding or parse error.
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$tokenAfterNameEnd = $phpcsFile->findNext($exclude, $nameStart, $stopPoint);
|
||
|
|
||
|
if ($tokenAfterNameEnd === false) {
|
||
|
return $tokens[$nameStart]['content'];
|
||
|
}
|
||
|
|
||
|
// Name starts with number, so is composed of multiple tokens.
|
||
|
return GetTokensAsString::noEmpties($phpcsFile, $nameStart, ($tokenAfterNameEnd - 1));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the implementation properties of a class.
|
||
|
*
|
||
|
* Main differences with the PHPCS version:
|
||
|
* - Bugs fixed:
|
||
|
* - Handling of PHPCS annotations.
|
||
|
* - Handling of unorthodox docblock placement.
|
||
|
* - Defensive coding against incorrect calls to this method.
|
||
|
* - Additional `'abstract_token'`, `'final_token'`, and `'readonly_token'` indexes in the return array.
|
||
|
*
|
||
|
* @see \PHP_CodeSniffer\Files\File::getClassProperties() Original source.
|
||
|
* @see \PHPCSUtils\BackCompat\BCFile::getClassProperties() Cross-version compatible version of the original.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
||
|
* @param int $stackPtr The position in the stack of the `T_CLASS`
|
||
|
* token to acquire the properties for.
|
||
|
*
|
||
|
* @return array Array with implementation properties of a class.
|
||
|
* The format of the return value is:
|
||
|
* ```php
|
||
|
* array(
|
||
|
* 'is_abstract' => bool, // TRUE if the abstract keyword was found.
|
||
|
* 'abstract_token' => int|false, // The stack pointer to the `abstract` keyword or
|
||
|
* // FALSE if the abstract keyword was not found.
|
||
|
* 'is_final' => bool, // TRUE if the final keyword was found.
|
||
|
* 'final_token' => int|false, // The stack pointer to the `final` keyword or
|
||
|
* // FALSE if the abstract keyword was not found.
|
||
|
* 'is_readonly' => bool, // TRUE if the readonly keyword was found.
|
||
|
* 'readonly_token' => int|false, // The stack pointer to the `readonly` keyword or
|
||
|
* // FALSE if the abstract keyword was not found.
|
||
|
* );
|
||
|
* ```
|
||
|
*
|
||
|
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
|
||
|
* `T_CLASS` token.
|
||
|
*/
|
||
|
public static function getClassProperties(File $phpcsFile, $stackPtr)
|
||
|
{
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
|
||
|
if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CLASS) {
|
||
|
throw new RuntimeException('$stackPtr must be of type T_CLASS');
|
||
|
}
|
||
|
|
||
|
$valid = Collections::classModifierKeywords() + Tokens::$emptyTokens;
|
||
|
$properties = [
|
||
|
'is_abstract' => false,
|
||
|
'abstract_token' => false,
|
||
|
'is_final' => false,
|
||
|
'final_token' => false,
|
||
|
'is_readonly' => false,
|
||
|
'readonly_token' => false,
|
||
|
];
|
||
|
|
||
|
for ($i = ($stackPtr - 1); $i > 0; $i--) {
|
||
|
if (isset($valid[$tokens[$i]['code']]) === false) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
switch ($tokens[$i]['code']) {
|
||
|
case \T_ABSTRACT:
|
||
|
$properties['is_abstract'] = true;
|
||
|
$properties['abstract_token'] = $i;
|
||
|
break;
|
||
|
|
||
|
case \T_FINAL:
|
||
|
$properties['is_final'] = true;
|
||
|
$properties['final_token'] = $i;
|
||
|
break;
|
||
|
|
||
|
case \T_READONLY:
|
||
|
$properties['is_readonly'] = true;
|
||
|
$properties['readonly_token'] = $i;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $properties;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the name of the class that the specified class extends.
|
||
|
*
|
||
|
* Works for classes, anonymous classes and interfaces, though it is strongly recommended
|
||
|
* to use the {@see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedInterfaceNames()}
|
||
|
* method to examine interfaces instead. Interfaces can extend multiple parent interfaces,
|
||
|
* and that use-case is not handled by this method.
|
||
|
*
|
||
|
* Main differences with the PHPCS version:
|
||
|
* - Bugs fixed:
|
||
|
* - Handling of PHPCS annotations.
|
||
|
* - Handling of comments.
|
||
|
* - Handling of the namespace keyword used as operator.
|
||
|
* - Improved handling of parse errors.
|
||
|
* - The returned name will be clean of superfluous whitespace and/or comments.
|
||
|
* - Support for PHP 8.0 tokenization of identifier/namespaced names, cross-version PHP & PHPCS.
|
||
|
*
|
||
|
* @see \PHP_CodeSniffer\Files\File::findExtendedClassName() Original source.
|
||
|
* @see \PHPCSUtils\BackCompat\BCFile::findExtendedClassName() Cross-version compatible version of
|
||
|
* the original.
|
||
|
* @see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedInterfaceNames() Similar method for extended interfaces.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
||
|
* @param int $stackPtr The stack position of the class or interface.
|
||
|
*
|
||
|
* @return string|false The extended class name or `FALSE` on error or if there
|
||
|
* is no extended class name.
|
||
|
*/
|
||
|
public static function findExtendedClassName(File $phpcsFile, $stackPtr)
|
||
|
{
|
||
|
$names = self::findNames($phpcsFile, $stackPtr, \T_EXTENDS, Collections::ooCanExtend());
|
||
|
if ($names === false) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Classes can only extend one parent class.
|
||
|
return \array_shift($names);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the names of the interfaces that the specified class or enum implements.
|
||
|
*
|
||
|
* Main differences with the PHPCS version:
|
||
|
* - Bugs fixed:
|
||
|
* - Handling of PHPCS annotations.
|
||
|
* - Handling of comments.
|
||
|
* - Handling of the namespace keyword used as operator.
|
||
|
* - Improved handling of parse errors.
|
||
|
* - The returned name(s) will be clean of superfluous whitespace and/or comments.
|
||
|
* - Support for PHP 8.0 tokenization of identifier/namespaced names, cross-version PHP & PHPCS.
|
||
|
*
|
||
|
* @see \PHP_CodeSniffer\Files\File::findImplementedInterfaceNames() Original source.
|
||
|
* @see \PHPCSUtils\BackCompat\BCFile::findImplementedInterfaceNames() Cross-version compatible version of
|
||
|
* the original.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
||
|
* @param int $stackPtr The stack position of the class or enum token.
|
||
|
*
|
||
|
* @return array|false Array with names of the implemented interfaces or `FALSE` on
|
||
|
* error or if there are no implemented interface names.
|
||
|
*/
|
||
|
public static function findImplementedInterfaceNames(File $phpcsFile, $stackPtr)
|
||
|
{
|
||
|
return self::findNames($phpcsFile, $stackPtr, \T_IMPLEMENTS, Collections::ooCanImplement());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the names of the interfaces that the specified interface extends.
|
||
|
*
|
||
|
* @see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedClassName() Similar method for extended classes.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
|
||
|
* @param int $stackPtr The stack position of the interface keyword.
|
||
|
*
|
||
|
* @return array|false Array with names of the extended interfaces or `FALSE` on
|
||
|
* error or if there are no extended interface names.
|
||
|
*/
|
||
|
public static function findExtendedInterfaceNames(File $phpcsFile, $stackPtr)
|
||
|
{
|
||
|
return self::findNames(
|
||
|
$phpcsFile,
|
||
|
$stackPtr,
|
||
|
\T_EXTENDS,
|
||
|
[\T_INTERFACE => \T_INTERFACE]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the names of the extended classes or interfaces or the implemented
|
||
|
* interfaces that the specific class/interface declaration extends/implements.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
|
||
|
* @param int $stackPtr The stack position of the
|
||
|
* class/interface declaration keyword.
|
||
|
* @param int $keyword The token constant for the keyword to examine.
|
||
|
* Either `T_EXTENDS` or `T_IMPLEMENTS`.
|
||
|
* @param array $allowedFor Array of OO types for which use of the keyword
|
||
|
* is allowed.
|
||
|
*
|
||
|
* @return array|false Returns an array of names or `FALSE` on error or when the object
|
||
|
* being declared does not extend/implement another object.
|
||
|
*/
|
||
|
private static function findNames(File $phpcsFile, $stackPtr, $keyword, array $allowedFor)
|
||
|
{
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
|
||
|
if (isset($tokens[$stackPtr]) === false
|
||
|
|| isset($allowedFor[$tokens[$stackPtr]['code']]) === false
|
||
|
|| isset($tokens[$stackPtr]['scope_opener']) === false
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$scopeOpener = $tokens[$stackPtr]['scope_opener'];
|
||
|
$keywordPtr = $phpcsFile->findNext($keyword, ($stackPtr + 1), $scopeOpener);
|
||
|
if ($keywordPtr === false) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$find = Collections::namespacedNameTokens() + Tokens::$emptyTokens;
|
||
|
$names = [];
|
||
|
$end = $keywordPtr;
|
||
|
do {
|
||
|
$start = ($end + 1);
|
||
|
$end = $phpcsFile->findNext($find, $start, ($scopeOpener + 1), true);
|
||
|
$name = GetTokensAsString::noEmpties($phpcsFile, $start, ($end - 1));
|
||
|
|
||
|
if (\trim($name) !== '') {
|
||
|
$names[] = $name;
|
||
|
}
|
||
|
} while ($tokens[$end]['code'] === \T_COMMA);
|
||
|
|
||
|
if (empty($names)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return $names;
|
||
|
}
|
||
|
}
|