/** * Processes this test, when one of its tokens is encountered. * * @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 process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Skip broken function declarations. if (isset($token['scope_opener']) === false || isset($token['parenthesis_opener']) === false) { return; } $params = array(); foreach ($phpcsFile->getMethodParameters($stackPtr) as $param) { $params[$param['name']] = $stackPtr; } $next = ++$token['scope_opener']; $end = --$token['scope_closer']; $foundContent = false; $validTokens = array(T_HEREDOC => T_HEREDOC, T_NOWDOC => T_NOWDOC, T_END_HEREDOC => T_END_HEREDOC, T_END_NOWDOC => T_END_NOWDOC, T_DOUBLE_QUOTED_STRING => T_DOUBLE_QUOTED_STRING); $validTokens += Tokens::$emptyTokens; for (; $next <= $end; ++$next) { $token = $tokens[$next]; $code = $token['code']; // Ignorable tokens. if (isset(Tokens::$emptyTokens[$code]) === true) { continue; } if ($foundContent === false) { // A throw statement as the first content indicates an interface method. if ($code === T_THROW) { return; } // A return statement as the first content indicates an interface method. if ($code === T_RETURN) { $tmp = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); if ($tmp === false) { return; } // There is a return. if ($tokens[$tmp]['code'] === T_SEMICOLON) { return; } $tmp = $phpcsFile->findNext(Tokens::$emptyTokens, $tmp + 1, null, true); if ($tmp !== false && $tokens[$tmp]['code'] === T_SEMICOLON) { // There is a return <token>. return; } } //end if } //end if $foundContent = true; if ($code === T_VARIABLE && isset($params[$token['content']]) === true) { unset($params[$token['content']]); } else { if ($code === T_DOLLAR) { $nextToken = $phpcsFile->findNext(T_WHITESPACE, $next + 1, null, true); if ($tokens[$nextToken]['code'] === T_OPEN_CURLY_BRACKET) { $nextToken = $phpcsFile->findNext(T_WHITESPACE, $nextToken + 1, null, true); if ($tokens[$nextToken]['code'] === T_STRING) { $varContent = '$' . $tokens[$nextToken]['content']; if (isset($params[$varContent]) === true) { unset($params[$varContent]); } } } } else { if ($code === T_DOUBLE_QUOTED_STRING || $code === T_START_HEREDOC || $code === T_START_NOWDOC) { // Tokenize strings that can contain variables. // Make sure the string is re-joined if it occurs over multiple lines. $content = $token['content']; for ($i = $next + 1; $i <= $end; $i++) { if (isset($validTokens[$tokens[$i]['code']]) === true) { $content .= $tokens[$i]['content']; $next++; } else { break; } } $stringTokens = token_get_all(sprintf('<?php %s;?>', $content)); foreach ($stringTokens as $stringPtr => $stringToken) { if (is_array($stringToken) === false) { continue; } $varContent = ''; if ($stringToken[0] === T_DOLLAR_OPEN_CURLY_BRACES) { $varContent = '$' . $stringTokens[$stringPtr + 1][1]; } else { if ($stringToken[0] === T_VARIABLE) { $varContent = $stringToken[1]; } } if ($varContent !== '' && isset($params[$varContent]) === true) { unset($params[$varContent]); } } } } } //end if } //end for if ($foundContent === true && count($params) > 0) { foreach ($params as $paramName => $position) { $error = 'The method parameter %s is never used'; $data = array($paramName); $phpcsFile->addWarning($error, $position, 'Found', $data); } } }
/** * 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); } }
/** * 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) { $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 = ''; 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]; // 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) { $comment .= ' ' . $tokens[$i]['content']; } } } else { $error = 'Missing parameter comment'; $phpcsFile->addError($error, $tag, 'MissingParamComment'); } } 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, '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 ($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) { $content = $param['type']; $content .= str_repeat(' ', $spaces); $content .= $param['var']; $content .= str_repeat(' ', $param['var_space']); $content .= $param['comment']; $phpcsFile->fixer->replaceToken($param['tag'] + 2, $content); } } // 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) { $content = $param['type']; $content .= str_repeat(' ', $param['type_space']); $content .= $param['var']; $content .= str_repeat(' ', $spaces); $content .= $param['comment']; $phpcsFile->fixer->replaceToken($param['tag'] + 2, $content); } } } //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); } }
/** * Processes this test, when one of its tokens is encountered. * * @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 process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Skip function without body. if (isset($token['scope_opener']) === false) { return; } // Get function name. $methodName = $phpcsFile->getDeclarationName($stackPtr); // Get all parameters from method signature. $signature = array(); foreach ($phpcsFile->getMethodParameters($stackPtr) as $param) { $signature[] = $param['name']; } $next = ++$token['scope_opener']; $end = --$token['scope_closer']; for (; $next <= $end; ++$next) { $code = $tokens[$next]['code']; if (isset(Tokens::$emptyTokens[$code]) === true) { continue; } else { if ($code === T_RETURN) { continue; } } break; } // Any token except 'parent' indicates correct code. if ($tokens[$next]['code'] !== T_PARENT) { return; } // Find next non empty token index, should be double colon. $next = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); // Skip for invalid code. if ($next === false || $tokens[$next]['code'] !== T_DOUBLE_COLON) { return; } // Find next non empty token index, should be the function name. $next = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); // Skip for invalid code or other method. if ($next === false || $tokens[$next]['content'] !== $methodName) { return; } // Find next non empty token index, should be the open parenthesis. $next = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); // Skip for invalid code. if ($next === false || $tokens[$next]['code'] !== T_OPEN_PARENTHESIS) { return; } $validParameterTypes = array(T_VARIABLE, T_LNUMBER, T_CONSTANT_ENCAPSED_STRING); $parameters = array(''); $parenthesisCount = 1; $count = count($tokens); for (++$next; $next < $count; ++$next) { $code = $tokens[$next]['code']; if ($code === T_OPEN_PARENTHESIS) { ++$parenthesisCount; } else { if ($code === T_CLOSE_PARENTHESIS) { --$parenthesisCount; } else { if ($parenthesisCount === 1 && $code === T_COMMA) { $parameters[] = ''; } else { if (isset(Tokens::$emptyTokens[$code]) === false) { $parameters[count($parameters) - 1] .= $tokens[$next]['content']; } } } } if ($parenthesisCount === 0) { break; } } //end for $next = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); if ($next === false || $tokens[$next]['code'] !== T_SEMICOLON) { return; } // Check rest of the scope. for (++$next; $next <= $end; ++$next) { $code = $tokens[$next]['code']; // Skip for any other content. if (isset(Tokens::$emptyTokens[$code]) === false) { return; } } $parameters = array_map('trim', $parameters); $parameters = array_filter($parameters); if (count($parameters) === count($signature) && $parameters === $signature) { $phpcsFile->addWarning('Possible useless method overriding detected', $stackPtr, 'Found'); } }