protected function FunctionCommentSniff::processParams in Coder 8.2
Same name and namespace in other branches
- 8.3 coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php \Drupal\Sniffs\Commenting\FunctionCommentSniff::processParams()
- 8.3.x coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php \Drupal\Sniffs\Commenting\FunctionCommentSniff::processParams()
Process the function parameter comments.
Parameters
\PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.:
int $stackPtr The position of the current token: in the stack passed in $tokens.
int $commentStart The position in the stack where the comment started.:
Return value
void
1 call to FunctionCommentSniff::processParams()
- FunctionCommentSniff::process in coder_sniffer/
Drupal/ Sniffs/ Commenting/ FunctionCommentSniff.php - Processes this test, when one of its tokens is encountered.
File
- coder_sniffer/
Drupal/ Sniffs/ Commenting/ FunctionCommentSniff.php, line 461
Class
- FunctionCommentSniff
- Parses and verifies the doc comments for functions. Largely copied from PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff.
Namespace
Drupal\Sniffs\CommentingCode
protected function processParams(File $phpcsFile, $stackPtr, $commentStart) {
$tokens = $phpcsFile
->getTokens();
$params = array();
$maxType = 0;
$maxVar = 0;
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($tokens[$tag]['content'] !== '@param') {
continue;
}
$type = '';
$typeSpace = 0;
$var = '';
$varSpace = 0;
$comment = '';
$commentLines = array();
if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) {
$matches = array();
preg_match('/([^$&]*)(?:((?:\\$|&)[^\\s]+)(?:(\\s+)(.*))?)?/', $tokens[$tag + 2]['content'], $matches);
$typeLen = strlen($matches[1]);
$type = trim($matches[1]);
$typeSpace = $typeLen - strlen($type);
$typeLen = strlen($type);
if ($typeLen > $maxType) {
$maxType = $typeLen;
}
// If there is more than one word then it is a comment that should be
// on the next line.
if (isset($matches[4]) === true && ($typeLen > 0 || preg_match('/[^\\s]+[\\s]+[^\\s]+/', $matches[4]) === 1)) {
$comment = $matches[4];
$error = 'Parameter comment must be on the next line';
$fix = $phpcsFile
->addFixableError($error, $tag + 2, 'ParamCommentNewLine');
if ($fix === true) {
$parts = $matches;
unset($parts[0]);
$parts[3] = "\n * ";
$phpcsFile->fixer
->replaceToken($tag + 2, implode('', $parts));
}
}
if (isset($matches[2]) === true) {
$var = $matches[2];
}
else {
$var = '';
}
if (substr($var, -1) === '.') {
$error = 'Doc comment parameter name "%s" must not end with a dot';
$fix = $phpcsFile
->addFixableError($error, $tag + 2, 'ParamNameDot', [
$var,
]);
if ($fix === true) {
$content = $type . ' ' . substr($var, 0, -1);
$phpcsFile->fixer
->replaceToken($tag + 2, $content);
}
// Continue with the next parameter to avoid confusing
// overlapping errors further down.
continue;
}
$varLen = strlen($var);
if ($varLen > $maxVar) {
$maxVar = $varLen;
}
// Any strings until the next tag belong to this comment.
if (isset($tokens[$commentStart]['comment_tags'][$pos + 1]) === true) {
$end = $tokens[$commentStart]['comment_tags'][$pos + 1];
}
else {
$end = $tokens[$commentStart]['comment_closer'];
}
for ($i = $tag + 3; $i < $end; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
$indent = 0;
if ($tokens[$i - 1]['code'] === T_DOC_COMMENT_WHITESPACE) {
$indent = strlen($tokens[$i - 1]['content']);
}
$comment .= ' ' . $tokens[$i]['content'];
$commentLines[] = array(
'comment' => $tokens[$i]['content'],
'token' => $i,
'indent' => $indent,
);
if ($indent < 3) {
$error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
$fix = $phpcsFile
->addFixableError($error, $i, 'ParamCommentIndentation', array(
$indent,
));
if ($fix === true) {
$phpcsFile->fixer
->replaceToken($i - 1, ' ');
}
}
}
}
//end for
// The first line of the comment must be indented no more than 3
// spaces, the following lines can be more so we only check the first
// line.
if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
$error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
$fix = $phpcsFile
->addFixableError($error, $commentLines[0]['token'] - 1, 'ParamCommentIndentation', array(
$commentLines[0]['indent'],
));
if ($fix === true) {
$phpcsFile->fixer
->replaceToken($commentLines[0]['token'] - 1, ' ');
}
}
if ($comment === '') {
$error = 'Missing parameter comment';
$phpcsFile
->addError($error, $tag, 'MissingParamComment');
$commentLines[] = array(
'comment' => '',
);
}
//end if
$variableArguments = false;
// Allow the "..." @param doc for a variable number of parameters.
// This could happen with type defined as @param array ... or
// without type defined as @param ...
if ($tokens[$tag + 2]['content'] === '...' || substr($tokens[$tag + 2]['content'], -3) === '...' && count(explode(' ', $tokens[$tag + 2]['content'])) === 2) {
$variableArguments = true;
}
if ($typeLen === 0) {
$error = 'Missing parameter type';
// If there is just one word as comment at the end of the line
// then this is probably the data type. Move it before the
// variable name.
if (isset($matches[4]) === true && preg_match('/[^\\s]+[\\s]+[^\\s]+/', $matches[4]) === 0) {
$fix = $phpcsFile
->addFixableError($error, $tag, 'MissingParamType');
if ($fix === true) {
$phpcsFile->fixer
->replaceToken($tag + 2, $matches[4] . ' ' . $var);
}
}
else {
$phpcsFile
->addError($error, $tag, 'MissingParamType');
}
}
if (empty($matches[2]) === true && $variableArguments === false) {
$error = 'Missing parameter name';
$phpcsFile
->addError($error, $tag, 'MissingParamName');
}
}
else {
$error = 'Missing parameter type';
$phpcsFile
->addError($error, $tag, 'MissingParamType');
}
//end if
$params[] = array(
'tag' => $tag,
'type' => $type,
'var' => $var,
'comment' => $comment,
'commentLines' => $commentLines,
'type_space' => $typeSpace,
'var_space' => $varSpace,
);
}
//end foreach
$realParams = $phpcsFile
->getMethodParameters($stackPtr);
$foundParams = array();
$checkPos = 0;
foreach ($params as $pos => $param) {
if ($param['var'] === '') {
continue;
}
$foundParams[] = $param['var'];
// If the type is empty, the whole line is empty.
if ($param['type'] === '') {
continue;
}
// Make sure the param name is correct.
$matched = false;
// Parameter documentation can be omitted for some parameters, so we have
// to search the rest for a match.
$realName = '<undefined>';
while (isset($realParams[$checkPos]) === true) {
$realName = $realParams[$checkPos]['name'];
if ($realName === $param['var'] || $realParams[$checkPos]['pass_by_reference'] === true && '&' . $realName === $param['var']) {
$matched = true;
break;
}
$checkPos++;
}
// Check the param type value. This could also be multiple parameter
// types separated by '|'.
$typeNames = explode('|', $param['type']);
$suggestedNames = array();
foreach ($typeNames as $i => $typeName) {
$suggestedNames[] = static::suggestType($typeName);
}
$suggestedType = implode('|', $suggestedNames);
if (preg_match('/\\s/', $param['type']) === 1) {
$error = 'Parameter type "%s" must not contain spaces';
$data = array(
$param['type'],
);
$phpcsFile
->addError($error, $param['tag'], 'ParamTypeSpaces', $data);
}
else {
if ($param['type'] !== $suggestedType) {
$error = 'Expected "%s" but found "%s" for parameter type';
$data = array(
$suggestedType,
$param['type'],
);
$fix = $phpcsFile
->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data);
if ($fix === true) {
$content = $suggestedType;
$content .= str_repeat(' ', $param['type_space']);
$content .= $param['var'];
$phpcsFile->fixer
->replaceToken($param['tag'] + 2, $content);
}
}
}
if (count($typeNames) === 1) {
$typeName = $param['type'];
$suggestedName = static::suggestType($typeName);
}
// This runs only if there is only one type name and the type name
// is not one of the disallowed type names.
if (count($typeNames) === 1 && $typeName === $suggestedName) {
// Check type hint for array and custom type.
$suggestedTypeHint = '';
if (strpos($suggestedName, 'array') !== false) {
$suggestedTypeHint = 'array';
}
else {
if (strpos($suggestedName, 'callable') !== false) {
$suggestedTypeHint = 'callable';
}
else {
if (substr($suggestedName, -2) === '[]') {
$suggestedTypeHint = 'array';
}
else {
if ($suggestedName === 'object') {
$suggestedTypeHint = '';
}
else {
if (in_array($typeName, $this->allowedTypes) === false) {
$suggestedTypeHint = $suggestedName;
}
}
}
}
}
if ($suggestedTypeHint !== '' && isset($realParams[$checkPos]) === true) {
$typeHint = $realParams[$checkPos]['type_hint'];
// Primitive type hints are allowed to be omitted.
if ($typeHint === '' && in_array($suggestedTypeHint, [
'string',
'int',
'float',
'bool',
]) === false) {
$error = 'Type hint "%s" missing for %s';
$data = array(
$suggestedTypeHint,
$param['var'],
);
$phpcsFile
->addError($error, $stackPtr, 'TypeHintMissing', $data);
}
else {
if ($typeHint !== $suggestedTypeHint && $typeHint !== '') {
// The type hint could be fully namespaced, so we check
// for the part after the last "\".
$name_parts = explode('\\', $suggestedTypeHint);
$last_part = end($name_parts);
if ($last_part !== $typeHint && $this
->isAliasedType($typeHint, $suggestedTypeHint, $phpcsFile) === false) {
$error = 'Expected type hint "%s"; found "%s" for %s';
$data = array(
$last_part,
$typeHint,
$param['var'],
);
$phpcsFile
->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
}
}
}
//end if
}
else {
if ($suggestedTypeHint === '' && isset($realParams[$checkPos]) === true) {
$typeHint = $realParams[$checkPos]['type_hint'];
if ($typeHint !== '' && $typeHint !== 'stdClass') {
$error = 'Unknown type hint "%s" found for %s';
$data = array(
$typeHint,
$param['var'],
);
$phpcsFile
->addError($error, $stackPtr, 'InvalidTypeHint', $data);
}
}
}
//end if
}
//end if
// Check number of spaces after the type.
$spaces = 1;
if ($param['type_space'] !== $spaces) {
$error = 'Expected %s spaces after parameter type; %s found';
$data = array(
$spaces,
$param['type_space'],
);
$fix = $phpcsFile
->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
if ($fix === true) {
$phpcsFile->fixer
->beginChangeset();
$content = $param['type'];
$content .= str_repeat(' ', $spaces);
$content .= $param['var'];
$content .= str_repeat(' ', $param['var_space']);
// At this point there is no description expected in the
// @param line so no need to append comment.
$phpcsFile->fixer
->replaceToken($param['tag'] + 2, $content);
// Fix up the indent of additional comment lines.
foreach ($param['commentLines'] as $lineNum => $line) {
if ($lineNum === 0 || $param['commentLines'][$lineNum]['indent'] === 0) {
continue;
}
$newIndent = $param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space'];
$phpcsFile->fixer
->replaceToken($param['commentLines'][$lineNum]['token'] - 1, str_repeat(' ', $newIndent));
}
$phpcsFile->fixer
->endChangeset();
}
//end if
}
//end if
if ($matched === false) {
if ($checkPos >= $pos) {
$code = 'ParamNameNoMatch';
$data = array(
$param['var'],
$realName,
);
$error = 'Doc comment for parameter %s does not match ';
if (strtolower($param['var']) === strtolower($realName)) {
$error .= 'case of ';
$code = 'ParamNameNoCaseMatch';
}
$error .= 'actual variable name %s';
$phpcsFile
->addError($error, $param['tag'], $code, $data);
// Reset the parameter position to check for following
// parameters.
$checkPos = $pos - 1;
}
else {
if (substr($param['var'], -4) !== ',...') {
// We must have an extra parameter comment.
$error = 'Superfluous parameter comment';
$phpcsFile
->addError($error, $param['tag'], 'ExtraParamComment');
}
}
//end if
}
//end if
$checkPos++;
if ($param['comment'] === '') {
continue;
}
// Param comments must start with a capital letter and end with the full stop.
if (isset($param['commentLines'][0]['comment']) === true) {
$firstChar = $param['commentLines'][0]['comment'];
}
else {
$firstChar = $param['comment'];
}
if (preg_match('|\\p{Lu}|u', $firstChar) === 0) {
$error = 'Parameter comment must start with a capital letter';
if (isset($param['commentLines'][0]['token']) === true) {
$commentToken = $param['commentLines'][0]['token'];
}
else {
$commentToken = $param['tag'];
}
$phpcsFile
->addError($error, $commentToken, 'ParamCommentNotCapital');
}
$lastChar = substr($param['comment'], -1);
if (in_array($lastChar, array(
'.',
'!',
'?',
')',
)) === false) {
$error = 'Parameter comment must end with a full stop';
if (empty($param['commentLines']) === true) {
$commentToken = $param['tag'] + 2;
}
else {
$lastLine = end($param['commentLines']);
$commentToken = $lastLine['token'];
}
$fix = $phpcsFile
->addFixableError($error, $commentToken, 'ParamCommentFullStop');
if ($fix === true) {
// Add a full stop as the last character of the comment.
$phpcsFile->fixer
->addContent($commentToken, '.');
}
}
}
//end foreach
// Missing parameters only apply to methods and not function because on
// functions it is allowed to leave out param comments for form constructors
// for example.
// It is also allowed to ommit pram tags completely, in which case we don't
// throw errors. Only throw errors if param comments exists but are
// incomplete on class methods.
if ($tokens[$stackPtr]['level'] > 0 && empty($foundParams) === false) {
foreach ($realParams as $realParam) {
$realParamKeyName = $realParam['name'];
if (in_array($realParamKeyName, $foundParams) === false && ($realParam['pass_by_reference'] === true && in_array("&{$realParamKeyName}", $foundParams) === true) === false) {
$error = 'Parameter %s is not described in comment';
$phpcsFile
->addError($error, $commentStart, 'ParamMissingDefinition', [
$realParam['name'],
]);
}
}
}
}