Mercurial > hg > isophonics-drupal-site
view vendor/drupal/coder/coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php @ 19:fa3358dc1485 tip
Add ndrum files
author | Chris Cannam |
---|---|
date | Wed, 28 Aug 2019 13:14:47 +0100 |
parents | af1871eacc83 |
children |
line wrap: on
line source
<?php /** * Parses and verifies the doc comments for functions. * * @category PHP * @package PHP_CodeSniffer * @link http://pear.php.net/package/PHP_CodeSniffer */ namespace Drupal\Sniffs\Commenting; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; /** * Parses and verifies the doc comments for functions. Largely copied from * PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff. * * @category PHP * @package PHP_CodeSniffer * @link http://pear.php.net/package/PHP_CodeSniffer */ class FunctionCommentSniff implements Sniff { /** * A map of invalid data types to valid ones for param and return documentation. * * @var array */ public static $invalidTypes = array( 'Array' => 'array', 'array()' => 'array', '[]' => 'array', 'boolean' => 'bool', 'Boolean' => 'bool', 'integer' => 'int', 'str' => 'string', 'stdClass' => 'object', 'number' => 'int', 'String' => 'string', 'type' => 'mixed', 'NULL' => 'null', 'FALSE' => 'false', 'TRUE' => 'true', 'Bool' => 'bool', 'Int' => 'int', 'Integer' => 'int', ); /** * An array of variable types for param/var we will check. * * @var array(string) */ public $allowedTypes = array( 'array', 'mixed', 'object', 'resource', 'callable', ); /** * Returns an array of tokens this test wants to listen for. * * @return array */ public function register() { return array(T_FUNCTION); }//end register() /** * Processes this test, when one of its tokens is encountered. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * * @return void */ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $find = Tokens::$methodPrefixes; $find[] = T_WHITESPACE; $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true); $beforeCommentEnd = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($commentEnd - 1), null, true); if (($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT) || ($beforeCommentEnd !== false // If there is something more on the line than just the comment then the // comment does not belong to the function. && $tokens[$beforeCommentEnd]['line'] === $tokens[$commentEnd]['line']) ) { $fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing'); if ($fix === true) { $before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true); $phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n"); } return; } if ($tokens[$commentEnd]['code'] === T_COMMENT) { $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle'); if ($fix === true) { // Convert the comment into a doc comment. $phpcsFile->fixer->beginChangeset(); $comment = ''; for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) { $comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment; $phpcsFile->fixer->replaceToken($i, ''); } $phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */\n"); $phpcsFile->fixer->endChangeset(); } return; } $commentStart = $tokens[$commentEnd]['comment_opener']; foreach ($tokens[$commentStart]['comment_tags'] as $tag) { // This is a file comment, not a function comment. if ($tokens[$tag]['content'] === '@file') { $fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing'); if ($fix === true) { $before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true); $phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n"); } return; } if ($tokens[$tag]['content'] === '@see') { // Make sure the tag isn't empty. $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { $error = 'Content missing for @see tag in function comment'; $phpcsFile->addError($error, $tag, 'EmptySees'); } } }//end foreach if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { $error = 'There must be no blank lines after the function comment'; $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter'); if ($fix === true) { $phpcsFile->fixer->replaceToken(($commentEnd + 1), ''); } } $this->processReturn($phpcsFile, $stackPtr, $commentStart); $this->processThrows($phpcsFile, $stackPtr, $commentStart); $this->processParams($phpcsFile, $stackPtr, $commentStart); $this->processSees($phpcsFile, $stackPtr, $commentStart); }//end process() /** * Process the return comment of this function comment. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) { $tokens = $phpcsFile->getTokens(); // Skip constructor and destructor. $className = ''; foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { if ($condition === T_CLASS || $condition === T_INTERFACE) { $className = $phpcsFile->getDeclarationName($condPtr); $className = strtolower(ltrim($className, '_')); } } $methodName = $phpcsFile->getDeclarationName($stackPtr); $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); $methodName = strtolower(ltrim($methodName, '_')); $return = null; foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { if ($tokens[$tag]['content'] === '@return') { if ($return !== null) { $error = 'Only 1 @return tag is allowed in a function comment'; $phpcsFile->addError($error, $tag, 'DuplicateReturn'); return; } $return = $tag; // 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']; } } } $type = null; if ($isSpecialMethod === false && $methodName !== $className) { if ($return !== null) { $type = trim($tokens[($return + 2)]['content']); if (empty($type) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { $error = 'Return type missing for @return tag in function comment'; $phpcsFile->addError($error, $return, 'MissingReturnType'); } else if (strpos($type, ' ') === false) { // Check return type (can be multiple, separated by '|'). $typeNames = explode('|', $type); $suggestedNames = array(); $hasNull = false; $hasMultiple = false; if (count($typeNames) > 0) { $hasMultiple = true; } foreach ($typeNames as $i => $typeName) { if (strtolower($typeName) === 'null') { $hasNull = true; } $suggestedName = static::suggestType($typeName); if (in_array($suggestedName, $suggestedNames) === false) { $suggestedNames[] = $suggestedName; } } $suggestedType = implode('|', $suggestedNames); if ($type !== $suggestedType) { $error = 'Expected "%s" but found "%s" for function return type'; $data = array( $suggestedType, $type, ); $fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data); if ($fix === true) { $content = $suggestedType; $phpcsFile->fixer->replaceToken(($return + 2), $content); } }//end if if ($type === 'void') { $error = 'If there is no return value for a function, there must not be a @return tag.'; $phpcsFile->addError($error, $return, 'VoidReturn'); } else if ($type !== 'mixed') { // If return type is not void, there needs to be a return statement // somewhere in the function that returns something. if (isset($tokens[$stackPtr]['scope_closer']) === true) { $endToken = $tokens[$stackPtr]['scope_closer']; $foundReturnToken = false; $searchStart = $stackPtr; $foundNonVoidReturn = false; do { $returnToken = $phpcsFile->findNext(array(T_RETURN, T_YIELD), $searchStart, $endToken); if ($returnToken === false && $foundReturnToken === false) { $error = '@return doc comment specified, but function has no return statement'; $phpcsFile->addError($error, $return, 'InvalidNoReturn'); } else { // Check for return token as the last loop after the last return // in the function will enter this else condition // but without the returnToken. if ($returnToken !== false) { $foundReturnToken = true; $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); if ($tokens[$semicolon]['code'] === T_SEMICOLON) { // Void return is allowed if the @return type has null in it. if ($hasNull === false) { $error = 'Function return type is not void, but function is returning void here'; $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid'); } } else { $foundNonVoidReturn = true; }//end if $searchStart = ($returnToken + 1); }//end if }//end if } while ($returnToken !== false); if ($foundNonVoidReturn === false && $foundReturnToken === true) { $error = 'Function return type is not void, but function does not have a non-void return statement'; $phpcsFile->addError($error, $return, 'InvalidReturnNotVoid'); } }//end if }//end if }//end if $comment = ''; for ($i = ($return + 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 = 'Return comment indentation must be 3 spaces, found %s spaces'; $fix = $phpcsFile->addFixableError($error, $i, 'ReturnCommentIndentation', 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 = 'Return comment indentation must be 3 spaces, found %s spaces'; $fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ReturnCommentIndentation', array($commentLines[0]['indent'])); if ($fix === true) { $phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' '); } } if ($comment === '' && $type !== '$this' && $type !== 'static') { if (strpos($type, ' ') !== false) { $error = 'Description for the @return value must be on the next line'; } else { $error = 'Description for the @return value is missing'; } $phpcsFile->addError($error, $return, 'MissingReturnComment'); } else if (strpos($type, ' ') !== false) { if (preg_match('/^([^\s]+)[\s]+(\$[^\s]+)[\s]*$/', $type, $matches) === 1) { $error = 'Return type must not contain variable name "%s"'; $data = array($matches[2]); $fix = $phpcsFile->addFixableError($error, ($return + 2), 'ReturnVarName', $data); if ($fix === true) { $phpcsFile->fixer->replaceToken(($return + 2), $matches[1]); } } else { $error = 'Return type "%s" must not contain spaces'; $data = array($type); $phpcsFile->addError($error, $return, 'ReturnTypeSpaces', $data); } }//end if }//end if } else { // No return tag for constructor and destructor. if ($return !== null) { $error = '@return tag is not required for constructor and destructor'; $phpcsFile->addError($error, $return, 'ReturnNotRequired'); } }//end if }//end processReturn() /** * Process any throw tags that this function comment has. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) { $tokens = $phpcsFile->getTokens(); foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { if ($tokens[$tag]['content'] !== '@throws') { continue; } if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { $error = 'Exception type missing for @throws tag in function comment'; $phpcsFile->addError($error, $tag, 'InvalidThrows'); } else { // 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']; } $comment = ''; $throwStart = null; for ($i = ($tag + 3); $i < $end; $i++) { if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { if ($throwStart === null) { $throwStart = $i; } $indent = 0; if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { $indent = strlen($tokens[($i - 1)]['content']); } $comment .= ' '.$tokens[$i]['content']; if ($indent < 3) { $error = 'Throws comment indentation must be 3 spaces, found %s spaces'; $phpcsFile->addError($error, $i, 'TrhowsCommentIndentation', array($indent)); } } } $comment = trim($comment); if ($comment === '') { if (str_word_count($tokens[($tag + 2)]['content'], 0, '\\_') > 1) { $error = '@throws comment must be on the next line'; $phpcsFile->addError($error, $tag, 'ThrowsComment'); } return; } // Starts with a capital letter and ends with a fullstop. $firstChar = $comment{0}; if (strtoupper($firstChar) !== $firstChar) { $error = '@throws tag comment must start with a capital letter'; $phpcsFile->addError($error, $throwStart, 'ThrowsNotCapital'); } $lastChar = substr($comment, -1); if (in_array($lastChar, array('.', '!', '?')) === false) { $error = '@throws tag comment must end with a full stop'; $phpcsFile->addError($error, $throwStart, 'ThrowsNoFullStop'); } }//end if }//end foreach }//end processThrows() /** * Process the function parameter comments. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ 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']]); } } } }//end processParams() /** * Process the function "see" comments. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processSees(File $phpcsFile, $stackPtr, $commentStart) { $tokens = $phpcsFile->getTokens(); foreach ($tokens[$commentStart]['comment_tags'] as $tag) { if ($tokens[$tag]['content'] !== '@see') { continue; } if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { $comment = $tokens[($tag + 2)]['content']; if (strpos($comment, ' ') !== false) { $error = 'The @see reference should not contain any additional text'; $phpcsFile->addError($error, $tag, 'SeeAdditionalText'); continue; } if (preg_match('/[\.!\?]$/', $comment) === 1) { $error = 'Trailing punctuation for @see references is not allowed.'; $fix = $phpcsFile->addFixableError($error, $tag, 'SeePunctuation'); if ($fix === true) { // Replace the last character from the comment which is // already tested to be a punctuation. $content = substr($comment, 0, -1); $phpcsFile->fixer->replaceToken(($tag + 2), $content); }//end if } } }//end foreach }//end processSees() /** * Returns a valid variable type for param/var tag. * * @param string $type The variable type to process. * * @return string */ public static function suggestType($type) { if (isset(static::$invalidTypes[$type]) === true) { return static::$invalidTypes[$type]; } if ($type === '$this') { return $type; } $type = preg_replace('/[^a-zA-Z0-9_\\\[\]]/', '', $type); return $type; }//end suggestType() /** * Checks if a used type hint is an alias defined by a "use" statement. * * @param string $typeHint The type hint used. * @param string $suggestedTypeHint The fully qualified type to * check against. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being checked. * * @return boolean */ protected function isAliasedType($typeHint, $suggestedTypeHint, File $phpcsFile) { $tokens = $phpcsFile->getTokens(); // Iterate over all "use" statements in the file. $usePtr = 0; while ($usePtr !== false) { $usePtr = $phpcsFile->findNext(T_USE, ($usePtr + 1)); if ($usePtr === false) { return false; } // Only check use statements in the global scope. if (empty($tokens[$usePtr]['conditions']) === false) { continue; } // Now comes the original class name, possibly with namespace // backslashes. $originalClass = $phpcsFile->findNext(Tokens::$emptyTokens, ($usePtr + 1), null, true); if ($originalClass === false || ($tokens[$originalClass]['code'] !== T_STRING && $tokens[$originalClass]['code'] !== T_NS_SEPARATOR) ) { continue; } $originalClassName = ''; while (in_array($tokens[$originalClass]['code'], array(T_STRING, T_NS_SEPARATOR)) === true) { $originalClassName .= $tokens[$originalClass]['content']; $originalClass++; } if (ltrim($originalClassName, '\\') !== ltrim($suggestedTypeHint, '\\')) { continue; } // Now comes the "as" keyword signaling an alias name for the class. $asPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($originalClass + 1), null, true); if ($asPtr === false || $tokens[$asPtr]['code'] !== T_AS) { continue; } // Now comes the name the class is aliased to. $aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($asPtr + 1), null, true); if ($aliasPtr === false || $tokens[$aliasPtr]['code'] !== T_STRING || $tokens[$aliasPtr]['content'] !== $typeHint ) { continue; } // We found a use statement that aliases the used type hint! return true; }//end while return false; }//end isAliasedType() }//end class