=> */ 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; } }