wishthis/vendor/phpcsstandards/phpcsutils/PHPCSUtils/Utils/UseStatements.php
2023-09-20 13:52:46 +02:00

432 lines
17 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\Internal\Cache;
use PHPCSUtils\Utils\Conditions;
use PHPCSUtils\Utils\Parentheses;
/**
* Utility functions for examining use statements.
*
* @since 1.0.0
*/
final class UseStatements
{
/**
* Determine what a T_USE 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_USE` token.
*
* @return string Either `'closure'`, `'import'` or `'trait'`.
* An empty string will be returned if the token is used in an
* invalid context or if it couldn't be reliably determined what
* the `T_USE` token is used for. An empty string being returned will
* normally mean the code being examined contains a parse error.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
* `T_USE` token.
*/
public static function getType(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]) === false
|| $tokens[$stackPtr]['code'] !== \T_USE
) {
throw new RuntimeException('$stackPtr must be of type T_USE');
}
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($next === false) {
// Live coding or parse error.
return '';
}
// More efficient & simpler check for closure use in PHPCS 4.x.
if (isset($tokens[$stackPtr]['parenthesis_owner'])
&& $tokens[$stackPtr]['parenthesis_owner'] === $stackPtr
) {
return 'closure';
}
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prev !== false && $tokens[$prev]['code'] === \T_CLOSE_PARENTHESIS
&& Parentheses::isOwnerIn($phpcsFile, $prev, \T_CLOSURE) === true
) {
return 'closure';
}
$lastCondition = Conditions::getLastCondition($phpcsFile, $stackPtr);
if ($lastCondition === false || $tokens[$lastCondition]['code'] === \T_NAMESPACE) {
// Global or scoped namespace and not a closure use statement.
return 'import';
}
$traitScopes = Tokens::$ooScopeTokens;
// Only classes, traits and enums can import traits.
unset($traitScopes[\T_INTERFACE]);
if (isset($traitScopes[$tokens[$lastCondition]['code']]) === true) {
return 'trait';
}
return '';
}
/**
* Determine whether a T_USE token represents a closure use statement.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the `T_USE` token.
*
* @return bool `TRUE` if the token passed is a closure use statement.
* `FALSE` if it's not.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
* `T_USE` token.
*/
public static function isClosureUse(File $phpcsFile, $stackPtr)
{
return (self::getType($phpcsFile, $stackPtr) === 'closure');
}
/**
* Determine whether a T_USE token represents a class/function/constant import use statement.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the `T_USE` token.
*
* @return bool `TRUE` if the token passed is an import use statement.
* `FALSE` if it's not.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
* `T_USE` token.
*/
public static function isImportUse(File $phpcsFile, $stackPtr)
{
return (self::getType($phpcsFile, $stackPtr) === 'import');
}
/**
* Determine whether a T_USE token represents a trait use statement.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the `T_USE` token.
*
* @return bool `TRUE` if the token passed is a trait use statement.
* `FALSE` if it's not.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
* `T_USE` token.
*/
public static function isTraitUse(File $phpcsFile, $stackPtr)
{
return (self::getType($phpcsFile, $stackPtr) === 'trait');
}
/**
* Split an import use statement into individual imports.
*
* Handles single import, multi-import and group-import use statements.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position in the stack of the `T_USE` token.
*
* @return array A multi-level array containing information about the use statement.
* The first level is `'name'`, `'function'` and `'const'`. These keys will always exist.
* If any statements are found for any of these categories, the second level
* will contain the alias/name as the key and the full original use name as the
* value for each of the found imports or an empty array if no imports were found
* in this use statement for a particular category.
*
* For example, for this function group use statement:
* ```php
* use function Vendor\Package\{
* LevelA\Name as Alias,
* LevelB\Another_Name,
* };
* ```
* the return value would look like this:
* ```php
* array(
* 'name' => array(),
* 'function' => array(
* 'Alias' => 'Vendor\Package\LevelA\Name',
* 'Another_Name' => 'Vendor\Package\LevelB\Another_Name',
* ),
* 'const' => array(),
* )
* ```
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
* `T_USE` token.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the `T_USE` token is not for an import
* use statement.
*/
public static function splitImportUseStatement(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_USE) {
throw new RuntimeException('$stackPtr must be of type T_USE');
}
if (self::isImportUse($phpcsFile, $stackPtr) === false) {
throw new RuntimeException('$stackPtr must be an import use statement');
}
if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) {
return Cache::get($phpcsFile, __METHOD__, $stackPtr);
}
$statements = [
'name' => [],
'function' => [],
'const' => [],
];
$endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
if ($endOfStatement === false) {
// Live coding or parse error.
Cache::set($phpcsFile, __METHOD__, $stackPtr, $statements);
return $statements;
}
++$endOfStatement;
$start = true;
$useGroup = false;
$hasAlias = false;
$baseName = '';
$name = '';
$type = '';
$fixedType = false;
$alias = '';
for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
continue;
}
$tokenCode = $tokens[$i]['code'];
switch ($tokenCode) {
case \T_STRING:
// Only when either at the start of the statement or at the start of a new sub within a group.
if ($start === true && $fixedType === false) {
$content = \strtolower($tokens[$i]['content']);
if ($content === 'function'
|| $content === 'const'
) {
$type = $content;
$start = false;
if ($useGroup === false) {
$fixedType = true;
}
break;
} else {
$type = 'name';
}
}
$start = false;
if ($hasAlias === false) {
$name .= $tokens[$i]['content'];
}
$alias = $tokens[$i]['content'];
break;
case \T_NAME_QUALIFIED:
case \T_NAME_FULLY_QUALIFIED: // This would be a parse error, but handle it anyway.
/*
* PHPCS 4.x.
*
* These tokens can only be encountered when either at the start of the statement
* or at the start of a new sub within a group.
*/
if ($start === true && $fixedType === false) {
$type = 'name';
}
$start = false;
if ($hasAlias === false) {
$name .= $tokens[$i]['content'];
}
$alias = \substr($tokens[$i]['content'], (\strrpos($tokens[$i]['content'], '\\') + 1));
break;
case \T_AS:
$hasAlias = true;
break;
case \T_OPEN_USE_GROUP:
$start = true;
$useGroup = true;
$baseName = $name;
$name = '';
break;
case \T_SEMICOLON:
case \T_CLOSE_TAG:
case \T_CLOSE_USE_GROUP:
case \T_COMMA:
if ($name !== '') {
if ($useGroup === true) {
$statements[$type][$alias] = $baseName . $name;
} else {
$statements[$type][$alias] = $name;
}
}
if ($tokenCode !== \T_COMMA) {
break 2;
}
// Reset.
$start = true;
$name = '';
$hasAlias = false;
if ($fixedType === false) {
$type = '';
}
break;
case \T_NS_SEPARATOR:
$name .= $tokens[$i]['content'];
break;
/*
* Fall back in case reserved keyword is (illegally) used in name.
* Parse error, but not our concern.
*/
default:
if ($hasAlias === false) {
// Defensive coding, just in case. Should no longer be possible since PHPCS 3.7.0.
$name .= $tokens[$i]['content']; // @codeCoverageIgnore
}
$alias = $tokens[$i]['content'];
break;
}
}
Cache::set($phpcsFile, __METHOD__, $stackPtr, $statements);
return $statements;
}
/**
* Split an import use statement into individual imports and merge it with an array of previously
* seen import use statements.
*
* Beware: this method should only be used to combine the import use statements found in *one* file.
* Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable.
*
* In most cases when tempted to use this method, the {@see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff}
* (upcoming) should be used instead.
*
* @see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff
* @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement()
* @see \PHPCSUtils\Utils\UseStatements::mergeImportUseStatements()
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position in the stack of the `T_USE` token.
* @param array $previousUseStatements The import `use` statements collected so far.
* This should be either the output of a
* previous call to this method or the output of
* an earlier call to the
* {@see UseStatements::splitImportUseStatement()}
* method.
*
* @return array A multi-level array containing information about the current `use` statement combined with
* the previously collected `use` statement information.
* See {@see UseStatements::splitImportUseStatement()} for more details about the array format.
*/
public static function splitAndMergeImportUseStatement(File $phpcsFile, $stackPtr, array $previousUseStatements)
{
try {
$useStatements = self::splitImportUseStatement($phpcsFile, $stackPtr);
$previousUseStatements = self::mergeImportUseStatements($previousUseStatements, $useStatements);
} catch (RuntimeException $e) {
// Not an import use statement.
}
return $previousUseStatements;
}
/**
* Merge two import use statement arrays.
*
* Beware: this method should only be used to combine the import use statements found in *one* file.
* Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable.
*
* @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement()
*
* @since 1.0.0
*
* @param array $previousUseStatements The import `use` statements collected so far.
* This should be either the output of a
* previous call to this method or the output of
* an earlier call to the
* {@see UseStatements::splitImportUseStatement()}
* method.
* @param array $currentUseStatement The parsed import `use` statements to merge with
* the previously collected use statements.
* This should be the output of a call to the
* {@see UseStatements::splitImportUseStatement()}
* method.
*
* @return array A multi-level array containing information about the current `use` statement combined with
* the previously collected `use` statement information.
* See {@see UseStatements::splitImportUseStatement()} for more details about the array format.
*/
public static function mergeImportUseStatements(array $previousUseStatements, array $currentUseStatement)
{
if (isset($previousUseStatements['name']) === false) {
$previousUseStatements['name'] = $currentUseStatement['name'];
} else {
$previousUseStatements['name'] += $currentUseStatement['name'];
}
if (isset($previousUseStatements['function']) === false) {
$previousUseStatements['function'] = $currentUseStatement['function'];
} else {
$previousUseStatements['function'] += $currentUseStatement['function'];
}
if (isset($previousUseStatements['const']) === false) {
$previousUseStatements['const'] = $currentUseStatement['const'];
} else {
$previousUseStatements['const'] += $currentUseStatement['const'];
}
return $previousUseStatements;
}
}