Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x

FunctionCommentSniff.php

Same filename in this branch
  1. 11.1.x vendor/drupal/coder/coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php
  2. 11.1.x vendor/squizlabs/php_codesniffer/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php
  3. 11.1.x vendor/squizlabs/php_codesniffer/src/Standards/MySource/Sniffs/Commenting/FunctionCommentSniff.php

Namespace

PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting

File

vendor/squizlabs/php_codesniffer/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php

View source
<?php


/**
 * Parses and verifies the doc comments for functions.
 *
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
 */
namespace PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class FunctionCommentSniff implements Sniff {
    
    /**
     * Disable the check for functions with a lower visibility than the value given.
     *
     * Allowed values are public, protected, and private.
     *
     * @var string
     */
    public $minimumVisibility = 'private';
    
    /**
     * Array of methods which do not require a return type.
     *
     * @var array
     */
    public $specialMethods = [
        '__construct',
        '__destruct',
    ];
    
    /**
     * Returns an array of tokens this test wants to listen for.
     *
     * @return array<int|string>
     */
    public function register() {
        return [
            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) {
        $scopeModifier = $phpcsFile->getMethodProperties($stackPtr)['scope'];
        if ($scopeModifier === 'protected' && $this->minimumVisibility === 'public' || $scopeModifier === 'private' && ($this->minimumVisibility === 'public' || $this->minimumVisibility === 'protected')) {
            return;
        }
        $tokens = $phpcsFile->getTokens();
        $ignore = Tokens::$methodPrefixes;
        $ignore[T_WHITESPACE] = T_WHITESPACE;
        for ($commentEnd = $stackPtr - 1; $commentEnd >= 0; $commentEnd--) {
            if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
                continue;
            }
            if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END && isset($tokens[$commentEnd]['attribute_opener']) === true) {
                $commentEnd = $tokens[$commentEnd]['attribute_opener'];
                continue;
            }
            break;
        }
        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
            // Inline comments might just be closing comments for
            // control structures or functions instead of function comments
            // using the wrong comment type. If there is other code on the line,
            // assume they relate to that code.
            $prev = $phpcsFile->findPrevious($ignore, $commentEnd - 1, null, true);
            if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) {
                $commentEnd = $prev;
            }
        }
        if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT) {
            $function = $phpcsFile->getDeclarationName($stackPtr);
            $phpcsFile->addError('Missing doc comment for function %s()', $stackPtr, 'Missing', [
                $function,
            ]);
            $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'no');
            return;
        }
        else {
            $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'yes');
        }
        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
            $phpcsFile->addError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
            return;
        }
        if ($tokens[$commentEnd]['line'] !== $tokens[$stackPtr]['line'] - 1) {
            for ($i = $commentEnd + 1; $i < $stackPtr; $i++) {
                if ($tokens[$i]['column'] !== 1) {
                    continue;
                }
                if ($tokens[$i]['code'] === T_WHITESPACE && $tokens[$i]['line'] !== $tokens[$i + 1]['line']) {
                    $error = 'There must be no blank lines after the function comment';
                    $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter');
                    if ($fix === true) {
                        $phpcsFile->fixer
                            ->beginChangeset();
                        while ($i < $stackPtr && $tokens[$i]['code'] === T_WHITESPACE && $tokens[$i]['line'] !== $tokens[$i + 1]['line']) {
                            $phpcsFile->fixer
                                ->replaceToken($i++, '');
                        }
                        $phpcsFile->fixer
                            ->endChangeset();
                    }
                    break;
                }
            }
            
            //end for
        }
        
        //end if
        $commentStart = $tokens[$commentEnd]['comment_opener'];
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
            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');
                }
            }
        }
        $this->processReturn($phpcsFile, $stackPtr, $commentStart);
        $this->processThrows($phpcsFile, $stackPtr, $commentStart);
        $this->processParams($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.
        $methodName = $phpcsFile->getDeclarationName($stackPtr);
        $isSpecialMethod = in_array($methodName, $this->specialMethods, true);
        $return = null;
        foreach ($tokens[$commentStart]['comment_tags'] as $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;
            }
        }
        if ($return !== null) {
            $content = $tokens[$return + 2]['content'];
            if (empty($content) === 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 ($isSpecialMethod === true) {
                return;
            }
            $error = 'Missing @return tag in function comment';
            $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
        }
        
        //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 $tag) {
            if ($tokens[$tag]['content'] !== '@throws') {
                continue;
            }
            $exception = null;
            if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) {
                $matches = [];
                preg_match('/([^\\s]+)(?:\\s+(.*))?/', $tokens[$tag + 2]['content'], $matches);
                $exception = $matches[1];
            }
            if ($exception === null) {
                $error = 'Exception type missing for @throws tag in function comment';
                $phpcsFile->addError($error, $tag, 'InvalidThrows');
            }
        }
        
        //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 = [];
        $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 = '';
            $commentEnd = 0;
            $commentTokens = [];
            if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) {
                $matches = [];
                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'];
                                $commentEnd = $i;
                                $commentTokens[] = $i;
                            }
                        }
                    }
                    else {
                        $error = 'Missing parameter comment';
                        $phpcsFile->addError($error, $tag, 'MissingParamComment');
                    }
                    
                    //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[] = [
                'tag' => $tag,
                'type' => $type,
                'var' => $var,
                'comment' => $comment,
                'comment_end' => $commentEnd,
                'comment_tokens' => $commentTokens,
                'type_space' => $typeSpace,
                'var_space' => $varSpace,
            ];
        }
        
        //end foreach
        $realParams = $phpcsFile->getMethodParameters($stackPtr);
        $foundParams = [];
        // We want to use ... for all variable length arguments, so add
        // 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'];
            if (trim($param['type']) !== '') {
                // 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 = [
                        $spaces,
                        $param['type_space'],
                    ];
                    $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
                    if ($fix === true) {
                        $commentToken = $param['tag'] + 2;
                        $content = $param['type'];
                        $content .= str_repeat(' ', $spaces);
                        $content .= $param['var'];
                        $content .= str_repeat(' ', $param['var_space']);
                        $wrapLength = $tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']);
                        $star = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
                        $spaceLength = strlen($content) + $tokens[$commentToken - 1]['length'] + $tokens[$commentToken - 2]['length'];
                        $padding = str_repeat(' ', $tokens[$star]['column'] - 1);
                        $padding .= '* ';
                        $padding .= str_repeat(' ', $spaceLength);
                        $content .= wordwrap($param['comment'], $wrapLength, $phpcsFile->eolChar . $padding);
                        $phpcsFile->fixer
                            ->replaceToken($commentToken, $content);
                        for ($i = $commentToken + 1; $i <= $param['comment_end']; $i++) {
                            $phpcsFile->fixer
                                ->replaceToken($i, '');
                        }
                    }
                    
                    //end if
                }
                
                //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 = [
                        $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 param name.
            $spaces = $maxVar - strlen($param['var']) + 1;
            if ($param['var_space'] !== $spaces) {
                $error = 'Expected %s spaces after parameter name; %s found';
                $data = [
                    $spaces,
                    $param['var_space'],
                ];
                $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data);
                if ($fix === true) {
                    $commentToken = $param['tag'] + 2;
                    $content = $param['type'];
                    $content .= str_repeat(' ', $param['type_space']);
                    $content .= $param['var'];
                    $content .= str_repeat(' ', $spaces);
                    $wrapLength = $tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']);
                    $star = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
                    $spaceLength = strlen($content) + $tokens[$commentToken - 1]['length'] + $tokens[$commentToken - 2]['length'];
                    $padding = str_repeat(' ', $tokens[$star]['column'] - 1);
                    $padding .= '* ';
                    $padding .= str_repeat(' ', $spaceLength);
                    $content .= wordwrap($param['comment'], $wrapLength, $phpcsFile->eolChar . $padding);
                    $phpcsFile->fixer
                        ->replaceToken($commentToken, $content);
                    for ($i = $commentToken + 1; $i <= $param['comment_end']; $i++) {
                        $phpcsFile->fixer
                            ->replaceToken($i, '');
                    }
                }
                
                //end if
            }
            
            //end if
            // Check the alignment of multi-line param comments.
            if ($param['tag'] !== $param['comment_end']) {
                $wrapLength = $tokens[$param['tag'] + 2]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']);
                $startColumn = $tokens[$param['tag'] + 2]['column'] + $tokens[$param['tag'] + 2]['length'] - $wrapLength;
                $star = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
                $expected = $startColumn - $tokens[$star]['column'] - 1;
                foreach ($param['comment_tokens'] as $commentToken) {
                    if ($tokens[$commentToken]['column'] === $startColumn) {
                        continue;
                    }
                    $found = 0;
                    if ($tokens[$commentToken - 1]['code'] === T_DOC_COMMENT_WHITESPACE) {
                        $found = $tokens[$commentToken - 1]['length'];
                    }
                    $error = 'Parameter comment not aligned correctly; expected %s spaces but found %s';
                    $data = [
                        $expected,
                        $found,
                    ];
                    if ($found < $expected) {
                        $code = 'ParamCommentAlignment';
                    }
                    else {
                        $code = 'ParamCommentAlignmentExceeded';
                    }
                    $fix = $phpcsFile->addFixableError($error, $commentToken, $code, $data);
                    if ($fix === true) {
                        $padding = str_repeat(' ', $expected);
                        if ($tokens[$commentToken - 1]['code'] === T_DOC_COMMENT_WHITESPACE) {
                            $phpcsFile->fixer
                                ->replaceToken($commentToken - 1, $padding);
                        }
                        else {
                            $phpcsFile->fixer
                                ->addContentBefore($commentToken, $padding);
                        }
                    }
                }
                
                //end foreach
            }
            
            //end if
        }
        
        //end foreach
        $realNames = [];
        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 = [
                $neededParam,
            ];
            $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data);
        }
    }
    
    //end processParams()

}

//end class

Classes

Title Deprecated Summary
FunctionCommentSniff

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal