/** * Called to process class member vars. * * @param PHP_CodeSniffer_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 processMemberVar(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $ignore = array(T_PUBLIC, T_PRIVATE, T_PROTECTED, T_VAR, T_STATIC, T_WHITESPACE); $commentEnd = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true); if ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT) { $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); return; } if ($tokens[$commentEnd]['code'] === T_COMMENT) { $phpcsFile->addError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle'); return; } $commentStart = $tokens[$commentEnd]['comment_opener']; $foundVar = null; foreach ($tokens[$commentStart]['comment_tags'] as $tag) { if ($tokens[$tag]['content'] === '@var') { if ($foundVar !== null) { $error = 'Only one @var tag is allowed in a member variable comment'; $phpcsFile->addError($error, $tag, 'DuplicateVar'); } else { $foundVar = $tag; } } else { 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 member variable comment'; $phpcsFile->addError($error, $tag, 'EmptySees'); } } else { $error = '%s tag is not allowed in member variable comment'; $data = array($tokens[$tag]['content']); $phpcsFile->addWarning($error, $tag, 'TagNotAllowed', $data); } } //end if } //end foreach // The @var tag is the only one we require. if ($foundVar === null) { $error = 'Missing @var tag in member variable comment'; $phpcsFile->addError($error, $commentEnd, 'MissingVar'); return; } $firstTag = $tokens[$commentStart]['comment_tags'][0]; if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') { $error = 'The @var tag must be the first tag in a member variable comment'; $phpcsFile->addError($error, $foundVar, 'VarOrder'); } // Make sure the tag isn't empty and has the correct padding. $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd); if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) { $error = 'Content missing for @var tag in member variable comment'; $phpcsFile->addError($error, $foundVar, 'EmptyVar'); return; } $varType = $tokens[$foundVar + 2]['content']; $suggestedType = Common::suggestType($varType); if ($varType !== $suggestedType) { $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; $data = array($suggestedType, $varType); $fix = $phpcsFile->addFixableError($error, $foundVar + 2, 'IncorrectVarType', $data); if ($fix === true) { $phpcsFile->fixer->replaceToken($foundVar + 2, $suggestedType); } } }
/** * Process the function parameter comments. * * @param PHP_CodeSniffer_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) { if ($this->phpVersion === null) { $this->phpVersion = Config::getConfigData('php_version'); if ($this->phpVersion === null) { $this->phpVersion = PHP_VERSION_ID; } } $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); if (empty($matches) === false) { $typeLen = strlen($matches[1]); $type = trim($matches[1]); $typeSpace = $typeLen - strlen($type); $typeLen = strlen($type); if ($typeLen > $maxType) { $maxType = $typeLen; } } if (isset($matches[2]) === true) { $var = $matches[2]; $varLen = strlen($var); if ($varLen > $maxVar) { $maxVar = $varLen; } if (isset($matches[4]) === true) { $varSpace = strlen($matches[3]); $comment = $matches[4]; $commentLines[] = array('comment' => $comment, 'token' => $tag + 2, 'indent' => $varSpace); // 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); } } } else { $error = 'Missing parameter comment'; $phpcsFile->addError($error, $tag, 'MissingParamComment'); $commentLines[] = array('comment' => ''); } //end if } else { $error = 'Missing parameter name'; $phpcsFile->addError($error, $tag, 'MissingParamName'); } //end if } 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(); // We want to use ... for all variable length arguments, so added // this prefix to the variable name so comparisons are easier. foreach ($realParams as $pos => $param) { if ($param['variable_length'] === true) { $realParams[$pos]['name'] = '...' . $realParams[$pos]['name']; } } foreach ($params as $pos => $param) { // If the type is empty, the whole line is empty. if ($param['type'] === '') { continue; } // Check the param type value. $typeNames = explode('|', $param['type']); foreach ($typeNames as $typeName) { $suggestedName = Common::suggestType($typeName); if ($typeName !== $suggestedName) { $error = 'Expected "%s" but found "%s" for parameter type'; $data = array($suggestedName, $typeName); $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); if ($fix === true) { $content = $suggestedName; $content .= str_repeat(' ', $param['type_space']); $content .= $param['var']; $content .= str_repeat(' ', $param['var_space']); if (isset($param['commentLines'][0]) === true) { $content .= $param['commentLines'][0]['comment']; } $phpcsFile->fixer->replaceToken($param['tag'] + 2, $content); } } else { if (count($typeNames) === 1) { // Check type hint for array and custom type. $suggestedTypeHint = ''; if (strpos($suggestedName, 'array') !== false || substr($suggestedName, -2) === '[]') { $suggestedTypeHint = 'array'; } else { if (strpos($suggestedName, 'callable') !== false) { $suggestedTypeHint = 'callable'; } else { if (strpos($suggestedName, 'callback') !== false) { $suggestedTypeHint = 'callable'; } else { if (in_array($typeName, Common::$allowedTypes) === false) { $suggestedTypeHint = $suggestedName; } else { if ($this->phpVersion >= 70000) { if ($typeName === 'string') { $suggestedTypeHint = 'string'; } else { if ($typeName === 'int' || $typeName === 'integer') { $suggestedTypeHint = 'int'; } else { if ($typeName === 'float') { $suggestedTypeHint = 'float'; } else { if ($typeName === 'bool' || $typeName === 'boolean') { $suggestedTypeHint = 'bool'; } } } } } } } } } if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) { $typeHint = $realParams[$pos]['type_hint']; if ($typeHint === '') { $error = 'Type hint "%s" missing for %s'; $data = array($suggestedTypeHint, $param['var']); $errorCode = 'TypeHintMissing'; if ($suggestedTypeHint === 'string' || $suggestedTypeHint === 'int' || $suggestedTypeHint === 'float' || $suggestedTypeHint === 'bool') { $errorCode = 'Scalar' . $errorCode; } $phpcsFile->addError($error, $stackPtr, $errorCode, $data); } else { if ($typeHint !== substr($suggestedTypeHint, strlen($typeHint) * -1)) { $error = 'Expected type hint "%s"; found "%s" for %s'; $data = array($suggestedTypeHint, $typeHint, $param['var']); $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); } } //end if } else { if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) { $typeHint = $realParams[$pos]['type_hint']; if ($typeHint !== '') { $error = 'Unknown type hint "%s" found for %s'; $data = array($typeHint, $param['var']); $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data); } } } //end if } } //end if } //end foreach if ($param['var'] === '') { continue; } $foundParams[] = $param['var']; // Check number of spaces after the type. $spaces = $maxType - strlen($param['type']) + 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']); $content .= $param['commentLines'][0]['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 // Make sure the param name is correct. if (isset($realParams[$pos]) === true) { $realName = $realParams[$pos]['name']; if ($realName !== $param['var']) { $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); } } 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 if ($param['comment'] === '') { continue; } // Check number of spaces after the var name. $spaces = $maxVar - strlen($param['var']) + 1; if ($param['var_space'] !== $spaces) { $error = 'Expected %s spaces after parameter name; %s found'; $data = array($spaces, $param['var_space']); $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data); if ($fix === true) { $phpcsFile->fixer->beginChangeset(); $content = $param['type']; $content .= str_repeat(' ', $param['type_space']); $content .= $param['var']; $content .= str_repeat(' ', $spaces); $content .= $param['commentLines'][0]['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['var_space']; $phpcsFile->fixer->replaceToken($param['commentLines'][$lineNum]['token'] - 1, str_repeat(' ', $newIndent)); } $phpcsFile->fixer->endChangeset(); } //end if } //end if // Param comments must start with a capital letter and end with the full stop. if (preg_match('/^(\\p{Ll}|\\P{L})/u', $param['comment']) === 1) { $error = 'Parameter comment must start with a capital letter'; $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital'); } $lastChar = substr($param['comment'], -1); if ($lastChar !== '.') { $error = 'Parameter comment must end with a full stop'; $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop'); } } //end foreach $realNames = array(); foreach ($realParams as $realParam) { $realNames[] = $realParam['name']; } // Report missing comments. $diff = array_diff($realNames, $foundParams); foreach ($diff as $neededParam) { $error = 'Doc comment for parameter "%s" missing'; $data = array($neededParam); $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data); } }