389 lines
15 KiB
PHP
389 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\BackCompat\BCFile;
|
|
use PHPCSUtils\Internal\Cache;
|
|
use PHPCSUtils\Tokens\Collections;
|
|
use PHPCSUtils\Utils\Conditions;
|
|
use PHPCSUtils\Utils\GetTokensAsString;
|
|
use PHPCSUtils\Utils\Parentheses;
|
|
|
|
/**
|
|
* Utility functions for use when examining T_NAMESPACE tokens and to determine the
|
|
* namespace of arbitrary tokens.
|
|
*
|
|
* @link https://www.php.net/language.namespaces PHP Manual on namespaces.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
final class Namespaces
|
|
{
|
|
|
|
/**
|
|
* Determine what a T_NAMESPACE token is used for.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of the `T_NAMESPACE` token.
|
|
*
|
|
* @return string Either `'declaration'`, `'operator'` or an empty string.
|
|
* An empty string will be returned if it couldn't be
|
|
* reliably determined what the `T_NAMESPACE` token is used for,
|
|
* which, in most cases, will mean the code contains a parse/fatal error.
|
|
*
|
|
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is
|
|
* not a `T_NAMESPACE` token.
|
|
*/
|
|
public static function getType(File $phpcsFile, $stackPtr)
|
|
{
|
|
static $findAfter;
|
|
|
|
if (isset($findAfter) === false) {
|
|
/*
|
|
* Set up array of tokens which can only be used in combination with the keyword as operator
|
|
* and which cannot be confused with other keywords.
|
|
*/
|
|
$findAfter = Tokens::$assignmentTokens
|
|
+ Tokens::$comparisonTokens
|
|
+ Tokens::$operators
|
|
+ Tokens::$castTokens
|
|
+ Tokens::$blockOpeners
|
|
+ Collections::incrementDecrementOperators()
|
|
+ Collections::objectOperators()
|
|
+ Collections::shortArrayListOpenTokensBC();
|
|
|
|
$findAfter[\T_OPEN_CURLY_BRACKET] = \T_OPEN_CURLY_BRACKET;
|
|
}
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_NAMESPACE) {
|
|
throw new RuntimeException('$stackPtr must be of type T_NAMESPACE');
|
|
}
|
|
|
|
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
|
|
if ($next === false) {
|
|
// Live coding or parse error.
|
|
return '';
|
|
}
|
|
|
|
if (empty($tokens[$stackPtr]['conditions']) === false
|
|
|| empty($tokens[$stackPtr]['nested_parenthesis']) === false
|
|
) {
|
|
/*
|
|
* Namespace declarations are only allowed at top level, so this can definitely not
|
|
* be a namespace declaration.
|
|
*/
|
|
if ($tokens[$next]['code'] === \T_NS_SEPARATOR) {
|
|
return 'operator';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
$start = BCFile::findStartOfStatement($phpcsFile, $stackPtr);
|
|
if ($start === $stackPtr
|
|
&& ($tokens[$next]['code'] === \T_STRING
|
|
|| $tokens[$next]['code'] === \T_NAME_QUALIFIED
|
|
|| $tokens[$next]['code'] === \T_OPEN_CURLY_BRACKET)
|
|
) {
|
|
return 'declaration';
|
|
}
|
|
|
|
if (($tokens[$next]['code'] === \T_NS_SEPARATOR
|
|
|| $tokens[$next]['code'] === \T_NAME_FULLY_QUALIFIED) // PHP 8.0 parse error.
|
|
&& ($start !== $stackPtr
|
|
|| $phpcsFile->findNext($findAfter, ($stackPtr + 1), null, false, null, true) !== false)
|
|
) {
|
|
return 'operator';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Determine whether a T_NAMESPACE token is the keyword for a namespace declaration.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of a `T_NAMESPACE` token.
|
|
*
|
|
* @return bool `TRUE` if the token passed is the keyword for a namespace declaration.
|
|
* `FALSE` if not.
|
|
*
|
|
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is
|
|
* not a `T_NAMESPACE` token.
|
|
*/
|
|
public static function isDeclaration(File $phpcsFile, $stackPtr)
|
|
{
|
|
return (self::getType($phpcsFile, $stackPtr) === 'declaration');
|
|
}
|
|
|
|
/**
|
|
* Determine whether a T_NAMESPACE token is used as an operator.
|
|
*
|
|
* @link https://www.php.net/language.namespaces.nsconstants PHP Manual about the use of the
|
|
* namespace keyword as an operator.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of a `T_NAMESPACE` token.
|
|
*
|
|
* @return bool `TRUE` if the namespace token passed is used as an operator. `FALSE` if not.
|
|
*
|
|
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is
|
|
* not a `T_NAMESPACE` token.
|
|
*/
|
|
public static function isOperator(File $phpcsFile, $stackPtr)
|
|
{
|
|
return (self::getType($phpcsFile, $stackPtr) === 'operator');
|
|
}
|
|
|
|
/**
|
|
* Get the complete namespace name as declared.
|
|
*
|
|
* For hierarchical namespaces, the namespace name will be composed of several tokens,
|
|
* i.e. "MyProject\Sub\Level", which will be returned together as one string.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of a `T_NAMESPACE` token.
|
|
* @param bool $clean Optional. Whether to get the name stripped
|
|
* of potentially interlaced whitespace and/or
|
|
* comments. Defaults to `true`.
|
|
*
|
|
* @return string|false The namespace name; or `FALSE` if the specified position is not a
|
|
* `T_NAMESPACE` token, the token points to a namespace operator
|
|
* or when parse errors are encountered/during live coding.
|
|
* > Note: The name can be an empty string for a valid global
|
|
* namespace declaration.
|
|
*/
|
|
public static function getDeclaredName(File $phpcsFile, $stackPtr, $clean = true)
|
|
{
|
|
try {
|
|
if (self::isDeclaration($phpcsFile, $stackPtr) === false) {
|
|
// Not a namespace declaration.
|
|
return false;
|
|
}
|
|
} catch (RuntimeException $e) {
|
|
// Non-existent token or not a namespace keyword token.
|
|
return false;
|
|
}
|
|
|
|
$endOfStatement = $phpcsFile->findNext(Collections::namespaceDeclarationClosers(), ($stackPtr + 1));
|
|
if ($endOfStatement === false) {
|
|
// Live coding or parse error.
|
|
return false;
|
|
}
|
|
|
|
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), ($endOfStatement + 1), true);
|
|
if ($next === $endOfStatement) {
|
|
// Declaration of global namespace. I.e.: namespace {}.
|
|
// If not a scoped {} namespace declaration, no name/global declarations are invalid
|
|
// and result in parse errors, but that's not our concern.
|
|
return '';
|
|
}
|
|
|
|
if ($clean === false) {
|
|
return \trim(GetTokensAsString::origContent($phpcsFile, $next, ($endOfStatement - 1)));
|
|
}
|
|
|
|
return \trim(GetTokensAsString::noEmpties($phpcsFile, $next, ($endOfStatement - 1)));
|
|
}
|
|
|
|
/**
|
|
* Find the stack pointer to the namespace declaration applicable for an arbitrary token.
|
|
*
|
|
* Take note:
|
|
* 1. When a namespace declaration token or a token which is part of the namespace
|
|
* name is passed to this method, the result will be `false` as technically, these tokens
|
|
* are not _within_ a namespace.
|
|
* 2. This method has no opinion on whether the token passed is actually _subject_
|
|
* to namespacing.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The token for which to determine
|
|
* the namespace.
|
|
*
|
|
* @return int|false Token pointer to the namespace keyword for the applicable namespace
|
|
* declaration; or `FALSE` if it couldn't be determined or
|
|
* if no namespace applies.
|
|
*/
|
|
public static function findNamespacePtr(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
// Check for the existence of the token.
|
|
if (isset($tokens[$stackPtr]) === false) {
|
|
return false;
|
|
}
|
|
|
|
// The namespace keyword in a namespace declaration is itself not namespaced.
|
|
if ($tokens[$stackPtr]['code'] === \T_NAMESPACE
|
|
&& self::isDeclaration($phpcsFile, $stackPtr) === true
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Check for scoped namespace {}.
|
|
$namespacePtr = Conditions::getCondition($phpcsFile, $stackPtr, \T_NAMESPACE);
|
|
if ($namespacePtr !== false) {
|
|
return $namespacePtr;
|
|
}
|
|
|
|
/*
|
|
* Not in a scoped namespace, so let's see if we can find a non-scoped namespace instead.
|
|
* Keeping in mind that:
|
|
* - there can be multiple non-scoped namespaces in a file (bad practice, but is allowed);
|
|
* - the namespace keyword can also be used as an operator;
|
|
* - a non-named namespace resolves to the global namespace;
|
|
* - and that namespace declarations can't be nested in anything, so we can skip over any
|
|
* nesting structures.
|
|
*/
|
|
if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) {
|
|
return Cache::get($phpcsFile, __METHOD__, $stackPtr);
|
|
}
|
|
|
|
// Start by breaking out of any scoped structures this token is in.
|
|
$prev = $stackPtr;
|
|
$firstCondition = Conditions::getFirstCondition($phpcsFile, $stackPtr);
|
|
if ($firstCondition !== false) {
|
|
$prev = $firstCondition;
|
|
}
|
|
|
|
// And break out of any surrounding parentheses as well.
|
|
$firstParensOpener = Parentheses::getFirstOpener($phpcsFile, $prev);
|
|
if ($firstParensOpener !== false) {
|
|
$prev = $firstParensOpener;
|
|
}
|
|
|
|
$find = [
|
|
\T_NAMESPACE,
|
|
\T_CLOSE_CURLY_BRACKET,
|
|
\T_CLOSE_PARENTHESIS,
|
|
\T_CLOSE_SHORT_ARRAY,
|
|
\T_CLOSE_SQUARE_BRACKET,
|
|
\T_DOC_COMMENT_CLOSE_TAG,
|
|
\T_ATTRIBUTE_END,
|
|
];
|
|
$returnValue = false;
|
|
|
|
do {
|
|
$prev = $phpcsFile->findPrevious($find, ($prev - 1));
|
|
if ($prev === false) {
|
|
break;
|
|
}
|
|
|
|
if ($tokens[$prev]['code'] === \T_CLOSE_CURLY_BRACKET) {
|
|
// Stop if we encounter a scoped namespace declaration as we already know we're not in one.
|
|
if (isset($tokens[$prev]['scope_condition']) === true
|
|
&& $tokens[$tokens[$prev]['scope_condition']]['code'] === \T_NAMESPACE
|
|
) {
|
|
break;
|
|
}
|
|
|
|
// Skip over other scoped structures for efficiency.
|
|
if (isset($tokens[$prev]['scope_condition']) === true) {
|
|
$prev = $tokens[$prev]['scope_condition'];
|
|
} elseif (isset($tokens[$prev]['scope_opener']) === true) {
|
|
// Shouldn't be possible, but just in case.
|
|
$prev = $tokens[$prev]['scope_opener']; // @codeCoverageIgnore
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Skip over other nesting structures for efficiency.
|
|
if (isset($tokens[$prev]['bracket_opener']) === true) {
|
|
$prev = $tokens[$prev]['bracket_opener'];
|
|
continue;
|
|
}
|
|
|
|
if (isset($tokens[$prev]['parenthesis_owner']) === true) {
|
|
$prev = $tokens[$prev]['parenthesis_owner'];
|
|
continue;
|
|
} elseif (isset($tokens[$prev]['parenthesis_opener']) === true) {
|
|
$prev = $tokens[$prev]['parenthesis_opener'];
|
|
continue;
|
|
}
|
|
|
|
// Skip over potentially large attributes.
|
|
if (isset($tokens[$prev]['attribute_opener'])) {
|
|
$prev = $tokens[$prev]['attribute_opener'];
|
|
continue;
|
|
}
|
|
|
|
// Skip over potentially large docblocks.
|
|
if (isset($tokens[$prev]['comment_opener'])) {
|
|
$prev = $tokens[$prev]['comment_opener'];
|
|
continue;
|
|
}
|
|
|
|
// So this is a namespace keyword, check if it's a declaration.
|
|
if ($tokens[$prev]['code'] === \T_NAMESPACE
|
|
&& self::isDeclaration($phpcsFile, $prev) === true
|
|
) {
|
|
// Now make sure the token was not part of the declaration.
|
|
$endOfStatement = $phpcsFile->findNext(Collections::namespaceDeclarationClosers(), ($prev + 1));
|
|
if ($endOfStatement > $stackPtr) {
|
|
// Token is part of the declaration, return false.
|
|
break;
|
|
}
|
|
|
|
$returnValue = $prev;
|
|
break;
|
|
}
|
|
} while (true);
|
|
|
|
Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue);
|
|
return $returnValue;
|
|
}
|
|
|
|
/**
|
|
* Determine the namespace name an arbitrary token lives in.
|
|
*
|
|
* Note: this method has no opinion on whether the token passed is actually _subject_
|
|
* to namespacing.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The token for which to determine
|
|
* the namespace.
|
|
*
|
|
* @return string Namespace name; or an empty string if the namespace couldn't be
|
|
* determined or when no namespace applies.
|
|
*/
|
|
public static function determineNamespace(File $phpcsFile, $stackPtr)
|
|
{
|
|
$namespacePtr = self::findNamespacePtr($phpcsFile, $stackPtr);
|
|
if ($namespacePtr === false) {
|
|
return '';
|
|
}
|
|
|
|
$namespace = self::getDeclaredName($phpcsFile, $namespacePtr);
|
|
if ($namespace !== false) {
|
|
return $namespace;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
}
|