Skip to content

Commit

Permalink
Merge pull request #235 from PHPCSStandards/feature/php-8-support-nam…
Browse files Browse the repository at this point in the history
…ed-parameters-in-function-calls

PassedParameters: add support for PHP 8.0 named arguments in function calls
  • Loading branch information
jrfnl committed Dec 1, 2020
2 parents b50df5f + 2c0abb2 commit 8655503
Show file tree
Hide file tree
Showing 6 changed files with 1,308 additions and 14 deletions.
175 changes: 161 additions & 14 deletions PHPCSUtils/Utils/PassedParameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
use PHPCSUtils\Utils\GetTokensAsString;
use PHPCSUtils\Utils\NamingConventions;

/**
* Utility functions to retrieve information about parameters passed to function calls,
Expand Down Expand Up @@ -144,6 +145,8 @@ public static function hasParameters(File $phpcsFile, $stackPtr)
* See {@see PassedParameters::hasParameters()} for information on the supported constructs.
*
* @since 1.0.0
* @since 1.0.0-alpha4 Added support for PHP 8.0 function calls with named arguments by
* introducing the new `'name_start'`, `'name_end'` and `'name'` index keys.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position of the `T_STRING`, PHP 8.0 identifier
Expand All @@ -160,6 +163,23 @@ public static function hasParameters(File $phpcsFile, $stackPtr)
* 'clean' => string, // Same as `raw`, but all comment tokens have been stripped out.
* )
* ```
* For function calls passing named arguments, the format is as follows:
* ```php
* 1 => array(
* 'name_start' => int, // The stack pointer to the first token in the parameter name.
* 'name_end' => int, // The stack pointer to the last token in the parameter name.
* // This will normally be the colon, but may be different in
* // PHPCS versions prior to the version adding support for
* // named parameters (PHPCS x.x.x).
* 'name' => string, // The parameter name as a string (without the colon).
* 'start' => int, // The stack pointer to the first token in the parameter value.
* 'end' => int, // The stack pointer to the last token in the parameter value.
* '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.
* )
* ```
* 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._
* If no parameters/array items are found, an empty array will be returned.
*
Expand All @@ -186,6 +206,8 @@ public static function getParameters(File $phpcsFile, $stackPtr)
$closer = $tokens[$opener]['parenthesis_closer'];
}

$mayHaveNames = (isset(Collections::functionCallTokens()[$tokens[$stackPtr]['code']]) === true);

$parameters = [];
$nextComma = $opener;
$paramStart = ($opener + 1);
Expand Down Expand Up @@ -234,7 +256,46 @@ public static function getParameters(File $phpcsFile, $stackPtr)
}

// Ok, we've reached the end of the parameter.
$paramEnd = ($nextComma - 1);
$paramEnd = ($nextComma - 1);

if ($mayHaveNames === true) {
$firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $paramStart, ($paramEnd + 1), true);
if ($firstNonEmpty !== $paramEnd) {
/*
* BC: Prior to support for named parameters being added to PHPCS in PHPCS 3.6.0 (?), the
* parameter name + the colon would in most cases be tokenized as one token: T_GOTO_LABEL.
*/
if ($tokens[$firstNonEmpty]['code'] === \T_GOTO_LABEL) {
$parameters[$cnt]['name_start'] = $paramStart;
$parameters[$cnt]['name_end'] = $firstNonEmpty;
$parameters[$cnt]['name'] = \substr($tokens[$firstNonEmpty]['content'], 0, -1);
$paramStart = ($firstNonEmpty + 1);
} else {
// PHPCS 3.6.0 (?) and select situations in PHPCS < 3.6.0 (?).
$secondNonEmpty = $phpcsFile->findNext(
Tokens::$emptyTokens,
($firstNonEmpty + 1),
($paramEnd + 1),
true
);

/*
* BC: Checking the content of the colon token instead of the token type as in PHPCS < 3.6.0 (?)
* the colon _may_ be tokenized as `T_STRING` or even `T_INLINE_ELSE`.
*/
if ($tokens[$secondNonEmpty]['content'] === ':'
&& ($tokens[$firstNonEmpty]['type'] === 'T_PARAM_NAME'
|| NamingConventions::isValidIdentifierName($tokens[$firstNonEmpty]['content']) === true)
) {
$parameters[$cnt]['name_start'] = $paramStart;
$parameters[$cnt]['name_end'] = $secondNonEmpty;
$parameters[$cnt]['name'] = $tokens[$firstNonEmpty]['content'];
$paramStart = ($secondNonEmpty + 1);
}
}
}
}

$parameters[$cnt]['start'] = $paramStart;
$parameters[$cnt]['end'] = $paramEnd;
$parameters[$cnt]['raw'] = \trim(GetTokensAsString::normal($phpcsFile, $paramStart, $paramEnd));
Expand Down Expand Up @@ -266,38 +327,60 @@ public static function getParameters(File $phpcsFile, $stackPtr)
*
* 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 the `T_STRING`, PHP 8.0 identifier
* name token, `T_VARIABLE`, `T_ARRAY`, `T_OPEN_SHORT_ARRAY`,
* `T_ISSET`, or `T_UNSET` 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.
* @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.
* The format of the return value is:
* ```php
* 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.
* )
* ```
* 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)
public static function getParameter(File $phpcsFile, $stackPtr, $paramOffset, $paramNames = [])
{
$tokens = $phpcsFile->getTokens();
$parameters = self::getParameters($phpcsFile, $stackPtr);

if (isset($parameters[$paramOffset]) === false) {
/*
* Non-function calls.
*/
if (isset(Collections::functionCallTokens()[$tokens[$stackPtr]['code']]) === false) {
if (isset($parameters[$paramOffset]) === true) {
return $parameters[$paramOffset];
}

return false;
}

return $parameters[$paramOffset];
/*
* Function calls.
*/
return self::getParameterFromStack($parameters, $paramOffset, $paramNames);
}

/**
Expand Down Expand Up @@ -325,4 +408,68 @@ public static function getParameterCount(File $phpcsFile, $stackPtr)

return \count(self::getParameters($phpcsFile, $stackPtr));
}

/**
* Get information on a specific function call parameter passed.
*
* This is an efficiency method to correcty 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 arrays 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 positional parameters.
if (isset($parameters[$paramOffset]) === true
&& isset($parameters[$paramOffset]['name']) === false
) {
return $parameters[$paramOffset];
}

$paramNames = \array_flip((array) $paramNames);
if (empty($paramNames) === true) {
throw new RuntimeException(
'To allow for support for PHP 8 named parameters, the $paramNames parameter must be passed.'
);
}

// Next check if a named parameter was passed with the specified name.
foreach ($parameters as $paramDetails) {
if (isset($paramDetails['name']) === false) {
continue;
}

if (isset($paramNames[$paramDetails['name']]) === true) {
return $paramDetails;
}
}

return false;
}
}
59 changes: 59 additions & 0 deletions Tests/Utils/PassedParameters/GetParameterFromStackTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/* testNoParams */
setcookie();

/* testIsset */
isset( $var1, $var2, $var3 );

/* testArray */
$array = array( $var1, $var2, $var3 );

/* testAllParamsPositional */
setcookie( 'name', 'value', time() + (60 * 60 * 24), '/path/', 'sub.example.com', true, false );

/* testAllParamsNamedStandardOrder */
setcookie(
name: 'name',
value: 'value',
expires_or_options: time() + (60 * 60 * 24),
path: '/path/',
domain: 'sub.example.com',
secure: true,
httponly: false
);

/* testAllParamsNamedNonStandardOrder */
setcookie(
expires_or_options: time() + (60 * 60 * 24),
secure: true,
name: 'name',
domain: 'sub.example.com',
value: 'value',
httponly: false,
path: '/path/',
);

/* testMixedPositionalAndNamedParams */
setcookie(
'name',
'value',
time() + (60 * 60 * 24),
secure: true,
path: '/path/',
httponly: false,
domain: 'sub.example.com',
);

/* testMixedPositionalAndNamedParamsNotAllOptionalSet */
setcookie(
'name',
expires_or_options: time() + (60 * 60 * 24),
secure: true,
);

/* testMixedPositionalAndNamedParamsOldName */
setcookie(
'name',
expires: time() + (60 * 60 * 24),
);
Loading

0 comments on commit 8655503

Please sign in to comment.