PHP_CodeSniffer
[ class tree: PHP_CodeSniffer ] [ index: PHP_CodeSniffer ] [ all elements ]

Source for file FunctionCommentSniff.php

Documentation is available at FunctionCommentSniff.php

  1. <?php
  2. /**
  3.  * Parses and verifies the doc comments for functions.
  4.  *
  5.  * @author    Greg Sherwood <gsherwood@squiz.net>
  6.  * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
  7.  * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  8.  */
  9.  
  10. namespace PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting;
  11.  
  12. use PHP_CodeSniffer\Sniffs\Sniff;
  13. use PHP_CodeSniffer\Files\File;
  14. use PHP_CodeSniffer\Util\Tokens;
  15.  
  16. class FunctionCommentSniff implements Sniff
  17. {
  18.  
  19.  
  20.     /**
  21.      * Returns an array of tokens this test wants to listen for.
  22.      *
  23.      * @return array 
  24.      */
  25.     public function register()
  26.     {
  27.         return array(T_FUNCTION);
  28.  
  29.     }//end register()
  30.  
  31.  
  32.     /**
  33.      * Processes this test, when one of its tokens is encountered.
  34.      *
  35.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  36.      * @param int                         $stackPtr  The position of the current token
  37.      *                                                in the stack passed in $tokens.
  38.      *
  39.      * @return void 
  40.      */
  41.     public function process(File $phpcsFile, $stackPtr)
  42.     {
  43.         $tokens = $phpcsFile->getTokens();
  44.         $find   = Tokens::$methodPrefixes;
  45.         $find[] = T_WHITESPACE;
  46.  
  47.         $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
  48.         if ($tokens[$commentEnd]['code'] === T_COMMENT) {
  49.             // Inline comments might just be closing comments for
  50.             // control structures or functions instead of function comments
  51.             // using the wrong comment type. If there is other code on the line,
  52.             // assume they relate to that code.
  53.             $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true);
  54.             if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) {
  55.                 $commentEnd = $prev;
  56.             }
  57.         }
  58.  
  59.         if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
  60.             && $tokens[$commentEnd]['code'] !== T_COMMENT
  61.         ) {
  62.             $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
  63.             $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'no');
  64.             return;
  65.         } else {
  66.             $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'yes');
  67.         }
  68.  
  69.         if ($tokens[$commentEnd]['code'] === T_COMMENT) {
  70.             $phpcsFile->addError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
  71.             return;
  72.         }
  73.  
  74.         if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
  75.             $error = 'There must be no blank lines after the function comment';
  76.             $phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
  77.         }
  78.  
  79.         $commentStart = $tokens[$commentEnd]['comment_opener'];
  80.         foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
  81.             if ($tokens[$tag]['content'] === '@see') {
  82.                 // Make sure the tag isn't empty.
  83.                 $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
  84.                 if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
  85.                     $error = 'Content missing for @see tag in function comment';
  86.                     $phpcsFile->addError($error, $tag, 'EmptySees');
  87.                 }
  88.             }
  89.         }
  90.  
  91.         $this->processReturn($phpcsFile, $stackPtr, $commentStart);
  92.         $this->processThrows($phpcsFile, $stackPtr, $commentStart);
  93.         $this->processParams($phpcsFile, $stackPtr, $commentStart);
  94.  
  95.     }//end process()
  96.  
  97.  
  98.     /**
  99.      * Process the return comment of this function comment.
  100.      *
  101.      * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
  102.      * @param int                         $stackPtr     The position of the current token
  103.      *                                                   in the stack passed in $tokens.
  104.      * @param int                         $commentStart The position in the stack where the comment started.
  105.      *
  106.      * @return void 
  107.      */
  108.     protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
  109.     {
  110.         $tokens = $phpcsFile->getTokens();
  111.  
  112.         // Skip constructor and destructor.
  113.         $methodName      = $phpcsFile->getDeclarationName($stackPtr);
  114.         $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct');
  115.  
  116.         $return = null;
  117.         foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
  118.             if ($tokens[$tag]['content'] === '@return') {
  119.                 if ($return !== null) {
  120.                     $error = 'Only 1 @return tag is allowed in a function comment';
  121.                     $phpcsFile->addError($error, $tag, 'DuplicateReturn');
  122.                     return;
  123.                 }
  124.  
  125.                 $return = $tag;
  126.             }
  127.         }
  128.  
  129.         if ($isSpecialMethod === true) {
  130.             return;
  131.         }
  132.  
  133.         if ($return !== null) {
  134.             $content = $tokens[($return + 2)]['content'];
  135.             if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  136.                 $error = 'Return type missing for @return tag in function comment';
  137.                 $phpcsFile->addError($error, $return, 'MissingReturnType');
  138.             }
  139.         } else {
  140.             $error = 'Missing @return tag in function comment';
  141.             $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
  142.         }//end if
  143.  
  144.     }//end processReturn()
  145.  
  146.  
  147.     /**
  148.      * Process any throw tags that this function comment has.
  149.      *
  150.      * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
  151.      * @param int                         $stackPtr     The position of the current token
  152.      *                                                   in the stack passed in $tokens.
  153.      * @param int                         $commentStart The position in the stack where the comment started.
  154.      *
  155.      * @return void 
  156.      */
  157.     protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
  158.     {
  159.         $tokens = $phpcsFile->getTokens();
  160.  
  161.         $throws = array();
  162.         foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
  163.             if ($tokens[$tag]['content'] !== '@throws') {
  164.                 continue;
  165.             }
  166.  
  167.             $exception = null;
  168.             $comment   = null;
  169.             if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
  170.                 $matches = array();
  171.                 preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches);
  172.                 $exception = $matches[1];
  173.                 if (isset($matches[2]) === true) {
  174.                     $comment = $matches[2];
  175.                 }
  176.             }
  177.  
  178.             if ($exception === null) {
  179.                 $error = 'Exception type missing for @throws tag in function comment';
  180.                 $phpcsFile->addError($error, $tag, 'InvalidThrows');
  181.             }
  182.         }//end foreach
  183.  
  184.     }//end processThrows()
  185.  
  186.  
  187.     /**
  188.      * Process the function parameter comments.
  189.      *
  190.      * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
  191.      * @param int                         $stackPtr     The position of the current token
  192.      *                                                   in the stack passed in $tokens.
  193.      * @param int                         $commentStart The position in the stack where the comment started.
  194.      *
  195.      * @return void 
  196.      */
  197.     protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
  198.     {
  199.         $tokens = $phpcsFile->getTokens();
  200.  
  201.         $params  = array();
  202.         $maxType = 0;
  203.         $maxVar  = 0;
  204.         foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
  205.             if ($tokens[$tag]['content'] !== '@param') {
  206.                 continue;
  207.             }
  208.  
  209.             $type          = '';
  210.             $typeSpace     = 0;
  211.             $var           = '';
  212.             $varSpace      = 0;
  213.             $comment       = '';
  214.             $commentEnd    = 0;
  215.             $commentTokens = array();
  216.  
  217.             if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
  218.                 $matches = array();
  219.                 preg_match('/([^$&.]+)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches);
  220.  
  221.                 if (empty($matches) === false) {
  222.                     $typeLen   = strlen($matches[1]);
  223.                     $type      = trim($matches[1]);
  224.                     $typeSpace = ($typeLen - strlen($type));
  225.                     $typeLen   = strlen($type);
  226.                     if ($typeLen > $maxType) {
  227.                         $maxType = $typeLen;
  228.                     }
  229.                 }
  230.  
  231.                 if (isset($matches[2]) === true) {
  232.                     $var    = $matches[2];
  233.                     $varLen = strlen($var);
  234.                     if ($varLen > $maxVar) {
  235.                         $maxVar = $varLen;
  236.                     }
  237.  
  238.                     if (isset($matches[4]) === true) {
  239.                         $varSpace = strlen($matches[3]);
  240.                         $comment  = $matches[4];
  241.  
  242.                         // Any strings until the next tag belong to this comment.
  243.                         if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
  244.                             $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
  245.                         } else {
  246.                             $end = $tokens[$commentStart]['comment_closer'];
  247.                         }
  248.  
  249.                         for ($i = ($tag + 3); $i < $end; $i++) {
  250.                             if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
  251.                                 $comment        .= ' '.$tokens[$i]['content'];
  252.                                 $commentEnd      = $i;
  253.                                 $commentTokens[] = $i;
  254.                             }
  255.                         }
  256.                     } else {
  257.                         $error = 'Missing parameter comment';
  258.                         $phpcsFile->addError($error, $tag, 'MissingParamComment');
  259.                     }//end if
  260.                 } else {
  261.                     $error = 'Missing parameter name';
  262.                     $phpcsFile->addError($error, $tag, 'MissingParamName');
  263.                 }//end if
  264.             } else {
  265.                 $error = 'Missing parameter type';
  266.                 $phpcsFile->addError($error, $tag, 'MissingParamType');
  267.             }//end if
  268.  
  269.             $params[] = array(
  270.                          'tag'            => $tag,
  271.                          'type'           => $type,
  272.                          'var'            => $var,
  273.                          'comment'        => $comment,
  274.                          'comment_end'    => $commentEnd,
  275.                          'comment_tokens' => $commentTokens,
  276.                          'type_space'     => $typeSpace,
  277.                          'var_space'      => $varSpace,
  278.                         );
  279.         }//end foreach
  280.  
  281.         $realParams  = $phpcsFile->getMethodParameters($stackPtr);
  282.         $foundParams = array();
  283.  
  284.         // We want to use ... for all variable length arguments, so add
  285.         // this prefix to the variable name so comparisons are easier.
  286.         foreach ($realParams as $pos => $param) {
  287.             if ($param['variable_length'] === true) {
  288.                 $realParams[$pos]['name'] = '...'.$realParams[$pos]['name'];
  289.             }
  290.         }
  291.  
  292.         foreach ($params as $pos => $param) {
  293.             if ($param['var'] === '') {
  294.                 continue;
  295.             }
  296.  
  297.             $foundParams[] = $param['var'];
  298.  
  299.             // Check number of spaces after the type.
  300.             $spaces = ($maxType - strlen($param['type']) + 1);
  301.             if ($param['type_space'] !== $spaces) {
  302.                 $error = 'Expected %s spaces after parameter type; %s found';
  303.                 $data  = array(
  304.                           $spaces,
  305.                           $param['type_space'],
  306.                          );
  307.  
  308.                 $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
  309.                 if ($fix === true) {
  310.                     $commentToken = ($param['tag'] + 2);
  311.  
  312.                     $content  = $param['type'];
  313.                     $content .= str_repeat(' ', $spaces);
  314.                     $content .= $param['var'];
  315.                     $content .= str_repeat(' ', $param['var_space']);
  316.  
  317.                     $wrapLength = ($tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']));
  318.  
  319.                     $star        = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
  320.                     $spaceLength = (strlen($content) + $tokens[($commentToken - 1)]['length'] + $tokens[($commentToken - 2)]['length']);
  321.  
  322.                     $padding  = str_repeat(' ', ($tokens[$star]['column'] - 1));
  323.                     $padding .= '* ';
  324.                     $padding .= str_repeat(' ', $spaceLength);
  325.  
  326.                     $content .= wordwrap(
  327.                         $param['comment'],
  328.                         $wrapLength,
  329.                         $phpcsFile->eolChar.$padding
  330.                     );
  331.  
  332.                     $phpcsFile->fixer->replaceToken($commentToken, $content);
  333.                     for ($i = ($commentToken + 1); $i <= $param['comment_end']; $i++) {
  334.                         $phpcsFile->fixer->replaceToken($i, '');
  335.                     }
  336.                 }//end if
  337.             }//end if
  338.  
  339.             // Make sure the param name is correct.
  340.             if (isset($realParams[$pos]) === true) {
  341.                 $realName = $realParams[$pos]['name'];
  342.                 if ($realName !== $param['var']) {
  343.                     $code = 'ParamNameNoMatch';
  344.                     $data = array(
  345.                              $param['var'],
  346.                              $realName,
  347.                             );
  348.  
  349.                     $error = 'Doc comment for parameter %s does not match ';
  350.                     if (strtolower($param['var']) === strtolower($realName)) {
  351.                         $error .= 'case of ';
  352.                         $code   = 'ParamNameNoCaseMatch';
  353.                     }
  354.  
  355.                     $error .= 'actual variable name %s';
  356.  
  357.                     $phpcsFile->addError($error, $param['tag'], $code, $data);
  358.                 }
  359.             } else if (substr($param['var'], -4) !== ',...') {
  360.                 // We must have an extra parameter comment.
  361.                 $error = 'Superfluous parameter comment';
  362.                 $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment');
  363.             }//end if
  364.  
  365.             if ($param['comment'] === '') {
  366.                 continue;
  367.             }
  368.  
  369.             // Check number of spaces after the param name.
  370.             $spaces = ($maxVar - strlen($param['var']) + 1);
  371.             if ($param['var_space'] !== $spaces) {
  372.                 $error = 'Expected %s spaces after parameter name; %s found';
  373.                 $data  = array(
  374.                           $spaces,
  375.                           $param['var_space'],
  376.                          );
  377.  
  378.                 $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data);
  379.                 if ($fix === true) {
  380.                     $commentToken = ($param['tag'] + 2);
  381.  
  382.                     $content  = $param['type'];
  383.                     $content .= str_repeat(' ', $param['type_space']);
  384.                     $content .= $param['var'];
  385.                     $content .= str_repeat(' ', $spaces);
  386.  
  387.                     $wrapLength = ($tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']));
  388.  
  389.                     $star        = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
  390.                     $spaceLength = (strlen($content) + $tokens[($commentToken - 1)]['length'] + $tokens[($commentToken - 2)]['length']);
  391.  
  392.                     $padding  = str_repeat(' ', ($tokens[$star]['column'] - 1));
  393.                     $padding .= '* ';
  394.                     $padding .= str_repeat(' ', $spaceLength);
  395.  
  396.                     $content .= wordwrap(
  397.                         $param['comment'],
  398.                         $wrapLength,
  399.                         $phpcsFile->eolChar.$padding
  400.                     );
  401.  
  402.                     $phpcsFile->fixer->replaceToken($commentToken, $content);
  403.                     for ($i = ($commentToken + 1); $i <= $param['comment_end']; $i++) {
  404.                         $phpcsFile->fixer->replaceToken($i, '');
  405.                     }
  406.                 }//end if
  407.             }//end if
  408.  
  409.             // Check the alignment of multi-line param comments.
  410.             if ($param['tag'] !== $param['comment_end']) {
  411.                 $wrapLength = ($tokens[($param['tag'] + 2)]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']));
  412.  
  413.                 $startColumn = ($tokens[($param['tag'] + 2)]['column'] + $tokens[($param['tag'] + 2)]['length'] - $wrapLength);
  414.  
  415.                 $star     = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
  416.                 $expected = ($startColumn - $tokens[$star]['column'] - 1);
  417.  
  418.                 foreach ($param['comment_tokens'] as $commentToken) {
  419.                     if ($tokens[$commentToken]['column'] === $startColumn) {
  420.                         continue;
  421.                     }
  422.  
  423.                     $found = 0;
  424.                     if ($tokens[($commentToken - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
  425.                         $found = $tokens[($commentToken - 1)]['length'];
  426.                     }
  427.  
  428.                     $error = 'Parameter comment not aligned correctly; expected %s spaces but found %s';
  429.                     $data  = array(
  430.                               $expected,
  431.                               $found,
  432.                              );
  433.                     $fix   = $phpcsFile->addFixableError($error, $commentToken, 'ParamCommentAlignment', $data);
  434.                     if ($fix === true) {
  435.                         $padding = str_repeat(' ', $expected);
  436.                         if ($tokens[($commentToken - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
  437.                             $phpcsFile->fixer->replaceToken(($commentToken - 1), $padding);
  438.                         } else {
  439.                             $phpcsFile->fixer->addContentBefore($commentToken, $padding);
  440.                         }
  441.                     }
  442.                 }//end foreach
  443.             }//end if
  444.         }//end foreach
  445.  
  446.         $realNames = array();
  447.         foreach ($realParams as $realParam) {
  448.             $realNames[] = $realParam['name'];
  449.         }
  450.  
  451.         // Report missing comments.
  452.         $diff = array_diff($realNames, $foundParams);
  453.         foreach ($diff as $neededParam) {
  454.             $error = 'Doc comment for parameter "%s" missing';
  455.             $data  = array($neededParam);
  456.             $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data);
  457.         }
  458.  
  459.     }//end processParams()
  460.  
  461.  
  462. }//end class

Documentation generated on Mon, 11 Mar 2019 15:27:31 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.