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

507 lines
23 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\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
use PHPCSUtils\Utils\GetTokensAsString;
/**
* Utility functions to retrieve information about parameters passed to function calls,
* class instantiations, array declarations, isset and unset constructs.
*
* @since 1.0.0
*/
final class PassedParameters
{
/**
* Tokens which are considered stop point, either because they are the end
* of the parameter (comma) or because we need to skip over them.
*
* @since 1.0.0
*
* @var array <int|string> => <int|string>
*/
private static $callParsingStopPoints = [
\T_COMMA => \T_COMMA,
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
\T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET,
\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
\T_DOC_COMMENT_OPEN_TAG => \T_DOC_COMMENT_OPEN_TAG,
\T_ATTRIBUTE => \T_ATTRIBUTE,
];
/**
* Checks if any parameters have been passed.
*
* - If passed a `T_STRING`, `T_NAME_FULLY_QUALIFIED`, `T_NAME_RELATIVE`, `T_NAME_QUALIFIED`,
* or `T_VARIABLE` stack pointer, it will treat it as a function call.
* If a `T_STRING` or `T_VARIABLE` which is *not* a function call is passed, the behaviour is
* undetermined.
* - If passed a `T_ANON_CLASS` stack pointer, it will accept it as a class instantiation.
* - If passed a `T_SELF`, `T_STATIC` or `T_PARENT` stack pointer, it will accept it as a
* class instantiation function call when used like `new self()` (with or without parenthesis).
* When these hierarchiecal keywords are not preceded by the `new` keyword, parenthesis
* will be required for the token to be accepted.
* - If passed a `T_ARRAY` or `T_OPEN_SHORT_ARRAY` stack pointer, it will detect
* whether the array has values or is empty.
* For purposes of backward-compatibility with older PHPCS versions, `T_OPEN_SQUARE_BRACKET`
* tokens will also be accepted and will be checked whether they are in reality
* a short array opener.
* - If passed a `T_ISSET` or `T_UNSET` stack pointer, it will detect whether those
* language constructs have "parameters".
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position of function call name,
* language construct or array open token.
* @param true|null $isShortArray Optional. Short-circuit the short array check for
* `T_OPEN_SHORT_ARRAY` tokens if it isn't necessary.
* Efficiency tweak for when this has already been established,
* Use with EXTREME care.
*
* @return bool
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the
* accepted types or doesn't exist.
*/
public static function hasParameters(File $phpcsFile, $stackPtr, $isShortArray = null)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]) === false
|| isset(Collections::parameterPassingTokens()[$tokens[$stackPtr]['code']]) === false
) {
throw new RuntimeException(
'The hasParameters() method expects a function call, array, isset or unset token to be passed.'
);
}
// Only accept self/static/parent if preceded by `new` or followed by an open parenthesis.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if (isset(Collections::ooHierarchyKeywords()[$tokens[$stackPtr]['code']]) === true) {
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$prev]['code'] !== \T_NEW
&& ($next !== false && $tokens[$next]['code'] !== \T_OPEN_PARENTHESIS)
) {
throw new RuntimeException(
'The hasParameters() method expects a function call, array, isset or unset token to be passed.'
);
}
}
if (isset(Collections::shortArrayListOpenTokensBC()[$tokens[$stackPtr]['code']]) === true
&& $isShortArray !== true
&& Arrays::isShortArray($phpcsFile, $stackPtr) === false
) {
throw new RuntimeException(
'The hasParameters() method expects a function call, array, isset or unset token to be passed.'
);
}
if ($next === false) {
return false;
}
// Deal with short array syntax.
if (isset(Collections::shortArrayListOpenTokensBC()[$tokens[$stackPtr]['code']]) === true) {
if ($next === $tokens[$stackPtr]['bracket_closer']) {
// No parameters.
return false;
}
return true;
}
// Deal with function calls, long arrays, isset and unset.
// Next non-empty token should be the open parenthesis.
if ($tokens[$next]['code'] !== \T_OPEN_PARENTHESIS) {
return false;
}
if (isset($tokens[$next]['parenthesis_closer']) === false) {
return false;
}
$ignore = Tokens::$emptyTokens;
$ignore[\T_ELLIPSIS] = \T_ELLIPSIS; // Prevent PHP 8.1 first class callables from being seen as function calls.
$closeParenthesis = $tokens[$next]['parenthesis_closer'];
$nextNextNonEmpty = $phpcsFile->findNext($ignore, ($next + 1), ($closeParenthesis + 1), true);
if ($nextNextNonEmpty === $closeParenthesis) {
// No parameters.
return false;
}
return true;
}
/**
* Get information on all parameters passed.
*
* See {@see PassedParameters::hasParameters()} for information on the supported constructs.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position of function call name,
* language construct or array open token.
* @param int $limit Optional. Limit the parameter retrieval to the first #
* parameters/array entries.
* Use with care on function calls, as this can break
* support for named parameters!
* @param true|null $isShortArray Optional. Short-circuit the short array check for
* `T_OPEN_SHORT_ARRAY` tokens if it isn't necessary.
* Efficiency tweak for when this has already been established,
* Use with EXTREME care.
*
* @return array A multi-dimentional array with information on each parameter/array item.
* The information gathered about each parameter/array item is in the following format:
* ```php
* 1 => array(
* 'start' => int, // The stack pointer to the first token in the parameter/array item.
* 'end' => int, // The stack pointer to the last token in the parameter/array item.
* 'raw' => string, // A string with the contents of all tokens between `start` and `end`.
* 'clean' => string, // Same as `raw`, but all comment tokens have been stripped out.
* )
* ```
* If a named parameter is encountered in a function call, the top-level index will not be
* the parameter _position_, but the _parameter name_ and the array will include two extra keys:
* ```php
* 'parameter_name' => array(
* 'name' => string, // The parameter name (without the colon).
* 'name_token' => int, // The stack pointer to the parameter name token.
* ...
* )
* ```
* The `'start'`, `'end'`, `'raw'` and `'clean'` indexes will always contain just and only
* information on the parameter value.
* _Note: The array starts at index 1 for positional parameters._
* _The key for named parameters will be the parameter name._
* If no parameters/array items are found, an empty array will be returned.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the
* accepted types or doesn't exist.
*/
public static function getParameters(File $phpcsFile, $stackPtr, $limit = 0, $isShortArray = null)
{
if (self::hasParameters($phpcsFile, $stackPtr, $isShortArray) === false) {
return [];
}
$effectiveLimit = (\is_int($limit) && $limit > 0) ? $limit : 0;
if (Cache::isCached($phpcsFile, __METHOD__, "$stackPtr-$effectiveLimit") === true) {
return Cache::get($phpcsFile, __METHOD__, "$stackPtr-$effectiveLimit");
}
if ($effectiveLimit !== 0 && Cache::isCached($phpcsFile, __METHOD__, "$stackPtr-0") === true) {
return \array_slice(Cache::get($phpcsFile, __METHOD__, "$stackPtr-0"), 0, $effectiveLimit, true);
}
// Ok, we know we have a valid token with parameters and valid open & close brackets/parenthesis.
$tokens = $phpcsFile->getTokens();
// Mark the beginning and end tokens.
if (isset(Collections::shortArrayListOpenTokensBC()[$tokens[$stackPtr]['code']]) === true) {
$opener = $stackPtr;
$closer = $tokens[$stackPtr]['bracket_closer'];
} else {
$opener = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
$closer = $tokens[$opener]['parenthesis_closer'];
}
$mayHaveNames = (isset(Collections::functionCallTokens()[$tokens[$stackPtr]['code']]) === true);
$parameters = [];
$nextComma = $opener;
$paramStart = ($opener + 1);
$cnt = 1;
$stopPoints = self::$callParsingStopPoints + Tokens::$scopeOpeners;
$stopPoints[] = $tokens[$closer]['code'];
while (($nextComma = $phpcsFile->findNext($stopPoints, ($nextComma + 1), ($closer + 1))) !== false) {
// Ignore anything within square brackets.
if (isset($tokens[$nextComma]['bracket_opener'], $tokens[$nextComma]['bracket_closer'])
&& $nextComma === $tokens[$nextComma]['bracket_opener']
) {
$nextComma = $tokens[$nextComma]['bracket_closer'];
continue;
}
// Skip past nested arrays, function calls and arbitrary groupings.
if ($tokens[$nextComma]['code'] === \T_OPEN_PARENTHESIS
&& isset($tokens[$nextComma]['parenthesis_closer'])
) {
$nextComma = $tokens[$nextComma]['parenthesis_closer'];
continue;
}
// Skip past closures, anonymous classes and anything else scope related.
if (isset($tokens[$nextComma]['scope_condition'], $tokens[$nextComma]['scope_closer'])
&& $tokens[$nextComma]['scope_condition'] === $nextComma
) {
$nextComma = $tokens[$nextComma]['scope_closer'];
continue;
}
// Skip over potentially large docblocks.
if ($tokens[$nextComma]['code'] === \T_DOC_COMMENT_OPEN_TAG
&& isset($tokens[$nextComma]['comment_closer'])
) {
$nextComma = $tokens[$nextComma]['comment_closer'];
continue;
}
// Skip over attributes.
if ($tokens[$nextComma]['code'] === \T_ATTRIBUTE
&& isset($tokens[$nextComma]['attribute_closer'])
) {
$nextComma = $tokens[$nextComma]['attribute_closer'];
continue;
}
if ($tokens[$nextComma]['code'] !== \T_COMMA
&& $tokens[$nextComma]['code'] !== $tokens[$closer]['code']
) {
// Just in case.
continue; // @codeCoverageIgnore
}
// Ok, we've reached the end of the parameter.
$paramEnd = ($nextComma - 1);
$key = $cnt;
if ($mayHaveNames === true) {
$firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $paramStart, ($paramEnd + 1), true);
if ($firstNonEmpty !== $paramEnd) {
$secondNonEmpty = $phpcsFile->findNext(
Tokens::$emptyTokens,
($firstNonEmpty + 1),
($paramEnd + 1),
true
);
if ($tokens[$secondNonEmpty]['code'] === \T_COLON
&& $tokens[$firstNonEmpty]['code'] === \T_PARAM_NAME
) {
if (isset($parameters[$tokens[$firstNonEmpty]['content']]) === false) {
// Set the key to be the name, but only if we've not seen this name before.
$key = $tokens[$firstNonEmpty]['content'];
}
$parameters[$key]['name'] = $tokens[$firstNonEmpty]['content'];
$parameters[$key]['name_token'] = $firstNonEmpty;
$paramStart = ($secondNonEmpty + 1);
}
}
}
$parameters[$key]['start'] = $paramStart;
$parameters[$key]['end'] = $paramEnd;
$parameters[$key]['raw'] = \trim(GetTokensAsString::normal($phpcsFile, $paramStart, $paramEnd));
$parameters[$key]['clean'] = \trim(GetTokensAsString::noComments($phpcsFile, $paramStart, $paramEnd));
// Check if there are more tokens before the closing parenthesis.
// Prevents function calls with trailing comma's from setting an extra parameter:
// `functionCall( $param1, $param2, );`.
$hasNextParam = $phpcsFile->findNext(
Tokens::$emptyTokens,
($nextComma + 1),
$closer,
true
);
if ($hasNextParam === false) {
// Reached the end, so for the purpose of caching, this should be saved as if no limit was set.
$effectiveLimit = 0;
break;
}
// Stop if there is a valid limit and the limit has been reached.
if ($effectiveLimit !== 0 && $cnt === $effectiveLimit) {
break;
}
// Prepare for the next parameter.
$paramStart = ($nextComma + 1);
++$cnt;
}
if ($effectiveLimit !== 0 && $cnt === $effectiveLimit) {
Cache::set($phpcsFile, __METHOD__, "$stackPtr-$effectiveLimit", $parameters);
} else {
// Limit is 0 or total items is less than effective limit.
Cache::set($phpcsFile, __METHOD__, "$stackPtr-0", $parameters);
}
return $parameters;
}
/**
* Get information on a specific parameter passed.
*
* See {@see PassedParameters::hasParameters()} for information on the supported constructs.
*
* @see PassedParameters::getParameterFromStack() For when the parameter stack of a function call is
* already retrieved.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position of function call name,
* language construct or array open token.
* @param int $paramOffset The 1-based index position of the parameter to retrieve.
* @param string|string[] $paramNames Optional. Either the name of the target parameter
* to retrieve as a string or an array of names for the
* same target parameter.
* Only relevant for function calls.
* An arrays of names is supported to allow for functions
* for which the parameter names have undergone name
* changes over time.
* When specified, the name will take precedence over the
* offset.
* For PHP 8 support, it is STRONGLY recommended to
* always pass both the offset as well as the parameter
* name when examining function calls.
*
* @return array|false Array with information on the parameter/array item at the specified offset,
* or with the specified name.
* Or `FALSE` if the specified parameter/array item is not found.
* See {@see PassedParameters::getParameters()} for the format of the returned
* (single-dimensional) array.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the
* accepted types or doesn't exist.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If a function call parameter is requested and
* the `$paramName` parameter is not passed.
*/
public static function getParameter(File $phpcsFile, $stackPtr, $paramOffset, $paramNames = [])
{
$tokens = $phpcsFile->getTokens();
if (empty($paramNames) === true) {
$parameters = self::getParameters($phpcsFile, $stackPtr, $paramOffset);
} else {
$parameters = self::getParameters($phpcsFile, $stackPtr);
}
/*
* Non-function calls.
*/
if (isset(Collections::functionCallTokens()[$tokens[$stackPtr]['code']]) === false) {
if (isset($parameters[$paramOffset]) === true) {
return $parameters[$paramOffset];
}
return false;
}
/*
* Function calls.
*/
return self::getParameterFromStack($parameters, $paramOffset, $paramNames);
}
/**
* Count the number of parameters which have been passed.
*
* See {@see PassedParameters::hasParameters()} for information on the supported constructs.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position of function call name,
* language construct or array open token.
*
* @return int
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the
* accepted types or doesn't exist.
*/
public static function getParameterCount(File $phpcsFile, $stackPtr)
{
if (self::hasParameters($phpcsFile, $stackPtr) === false) {
return 0;
}
return \count(self::getParameters($phpcsFile, $stackPtr));
}
/**
* Get information on a specific function call parameter passed.
*
* This is an efficiency method to correctly handle positional versus named parameters
* for function calls when multiple parameters need to be examined.
*
* See {@see PassedParameters::hasParameters()} for information on the supported constructs.
*
* @since 1.0.0
*
* @param array $parameters The output of a previous call to {@see PassedParameters::getParameters()}.
* @param int $paramOffset The 1-based index position of the parameter to retrieve.
* @param string|string[] $paramNames Either the name of the target parameter to retrieve
* as a string or an array of names for the same target parameter.
* An array of names is supported to allow for functions
* for which the parameter names have undergone name
* changes over time.
* The name will take precedence over the offset.
*
* @return array|false Array with information on the parameter at the specified offset,
* or with the specified name.
* Or `FALSE` if the specified parameter is not found.
* See {@see PassedParameters::getParameters()} for the format of the returned
* (single-dimensional) array.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the `$paramNames` parameter is not passed
* and the requested parameter was not passed
* as a positional parameter in the function call
* being examined.
*/
public static function getParameterFromStack(array $parameters, $paramOffset, $paramNames)
{
if (empty($parameters) === true) {
return false;
}
// First check for a named parameter.
if (empty($paramNames) === false) {
$paramNames = (array) $paramNames;
foreach ($paramNames as $name) {
// Note: parameter names are case-sensitive!.
if (isset($parameters[$name]) === true) {
return $parameters[$name];
}
}
}
// Next check for positional parameters.
if (isset($parameters[$paramOffset]) === true
&& isset($parameters[$paramOffset]['name']) === false
) {
return $parameters[$paramOffset];
}
if (empty($paramNames) === true) {
throw new RuntimeException(
'To allow for support for PHP 8 named parameters, the $paramNames parameter must be passed.'
);
}
return false;
}
}