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

Source for file FileCommentSniff.php

Documentation is available at FileCommentSniff.php

  1. <?php
  2. /**
  3.  * Parses and verifies the doc comments for files.
  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\Common;
  15.  
  16. class FileCommentSniff implements Sniff
  17. {
  18.  
  19.     /**
  20.      * Tags in correct order and related info.
  21.      *
  22.      * @var array 
  23.      */
  24.     protected $tags = array(
  25.                        '@category'   => array(
  26.                                          'required'       => true,
  27.                                          'allow_multiple' => false,
  28.                                         ),
  29.                        '@package'    => array(
  30.                                          'required'       => true,
  31.                                          'allow_multiple' => false,
  32.                                         ),
  33.                        '@subpackage' => array(
  34.                                          'required'       => false,
  35.                                          'allow_multiple' => false,
  36.                                         ),
  37.                        '@author'     => array(
  38.                                          'required'       => true,
  39.                                          'allow_multiple' => true,
  40.                                         ),
  41.                        '@copyright'  => array(
  42.                                          'required'       => false,
  43.                                          'allow_multiple' => true,
  44.                                         ),
  45.                        '@license'    => array(
  46.                                          'required'       => true,
  47.                                          'allow_multiple' => false,
  48.                                         ),
  49.                        '@version'    => array(
  50.                                          'required'       => false,
  51.                                          'allow_multiple' => false,
  52.                                         ),
  53.                        '@link'       => array(
  54.                                          'required'       => true,
  55.                                          'allow_multiple' => true,
  56.                                         ),
  57.                        '@see'        => array(
  58.                                          'required'       => false,
  59.                                          'allow_multiple' => true,
  60.                                         ),
  61.                        '@since'      => array(
  62.                                          'required'       => false,
  63.                                          'allow_multiple' => false,
  64.                                         ),
  65.                        '@deprecated' => array(
  66.                                          'required'       => false,
  67.                                          'allow_multiple' => false,
  68.                                         ),
  69.                       );
  70.  
  71.  
  72.     /**
  73.      * Returns an array of tokens this test wants to listen for.
  74.      *
  75.      * @return array 
  76.      */
  77.     public function register()
  78.     {
  79.         return array(T_OPEN_TAG);
  80.  
  81.     }//end register()
  82.  
  83.  
  84.     /**
  85.      * Processes this test, when one of its tokens is encountered.
  86.      *
  87.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  88.      * @param int                         $stackPtr  The position of the current token
  89.      *                                                in the stack passed in $tokens.
  90.      *
  91.      * @return int 
  92.      */
  93.     public function process(File $phpcsFile$stackPtr)
  94.     {
  95.         $tokens $phpcsFile->getTokens();
  96.  
  97.         // Find the next non whitespace token.
  98.         $commentStart $phpcsFile->findNext(T_WHITESPACE($stackPtr + 1)nulltrue);
  99.  
  100.         // Allow declare() statements at the top of the file.
  101.         if ($tokens[$commentStart]['code'=== T_DECLARE{
  102.             $semicolon    $phpcsFile->findNext(T_SEMICOLON($commentStart + 1));
  103.             $commentStart $phpcsFile->findNext(T_WHITESPACE($semicolon + 1)nulltrue);
  104.         }
  105.  
  106.         // Ignore vim header.
  107.         if ($tokens[$commentStart]['code'=== T_COMMENT{
  108.             if (strstr($tokens[$commentStart]['content']'vim:'!== false{
  109.                 $commentStart $phpcsFile->findNext(
  110.                     T_WHITESPACE,
  111.                     ($commentStart + 1),
  112.                     null,
  113.                     true
  114.                 );
  115.             }
  116.         }
  117.  
  118.         $errorToken ($stackPtr + 1);
  119.         if (isset($tokens[$errorToken]=== false{
  120.             $errorToken--;
  121.         }
  122.  
  123.         if ($tokens[$commentStart]['code'=== T_CLOSE_TAG{
  124.             // We are only interested if this is the first open tag.
  125.             return ($phpcsFile->numTokens + 1);
  126.         else if ($tokens[$commentStart]['code'=== T_COMMENT{
  127.             $error 'You must use "/**" style comments for a file comment';
  128.             $phpcsFile->addError($error$errorToken'WrongStyle');
  129.             $phpcsFile->recordMetric($stackPtr'File has doc comment''yes');
  130.             return ($phpcsFile->numTokens + 1);
  131.         else if ($commentStart === false
  132.             || $tokens[$commentStart]['code'!== T_DOC_COMMENT_OPEN_TAG
  133.         {
  134.             $phpcsFile->addError('Missing file doc comment'$errorToken'Missing');
  135.             $phpcsFile->recordMetric($stackPtr'File has doc comment''no');
  136.             return ($phpcsFile->numTokens + 1);
  137.         }
  138.  
  139.         $commentEnd $tokens[$commentStart]['comment_closer'];
  140.  
  141.         $nextToken $phpcsFile->findNext(
  142.             T_WHITESPACE,
  143.             ($commentEnd + 1),
  144.             null,
  145.             true
  146.         );
  147.  
  148.         $ignore = array(
  149.                    T_CLASS,
  150.                    T_INTERFACE,
  151.                    T_TRAIT,
  152.                    T_FUNCTION,
  153.                    T_CLOSURE,
  154.                    T_PUBLIC,
  155.                    T_PRIVATE,
  156.                    T_PROTECTED,
  157.                    T_FINAL,
  158.                    T_STATIC,
  159.                    T_ABSTRACT,
  160.                    T_CONST,
  161.                    T_PROPERTY,
  162.                   );
  163.  
  164.         if (in_array($tokens[$nextToken]['code']$ignore=== true{
  165.             $phpcsFile->addError('Missing file doc comment'$stackPtr'Missing');
  166.             $phpcsFile->recordMetric($stackPtr'File has doc comment''no');
  167.             return ($phpcsFile->numTokens + 1);
  168.         }
  169.  
  170.         $phpcsFile->recordMetric($stackPtr'File has doc comment''yes');
  171.  
  172.         // Check the PHP Version, which should be in some text before the first tag.
  173.         $found = false;
  174.         for ($i ($commentStart + 1)$i $commentEnd$i++{
  175.             if ($tokens[$i]['code'=== T_DOC_COMMENT_TAG{
  176.                 break;
  177.             else if ($tokens[$i]['code'=== T_DOC_COMMENT_STRING
  178.                 && strstr(strtolower($tokens[$i]['content'])'php version'!== false
  179.             {
  180.                 $found = true;
  181.                 break;
  182.             }
  183.         }
  184.  
  185.         if ($found === false{
  186.             $error 'PHP version not specified';
  187.             $phpcsFile->addWarning($error$commentEnd'MissingVersion');
  188.         }
  189.  
  190.         // Check each tag.
  191.         $this->processTags($phpcsFile$stackPtr$commentStart);
  192.  
  193.         // Ignore the rest of the file.
  194.         return ($phpcsFile->numTokens + 1);
  195.  
  196.     }//end process()
  197.  
  198.  
  199.     /**
  200.      * Processes each required or optional tag.
  201.      *
  202.      * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
  203.      * @param int                         $stackPtr     The position of the current token
  204.      *                                                   in the stack passed in $tokens.
  205.      * @param int                         $commentStart Position in the stack where the comment started.
  206.      *
  207.      * @return void 
  208.      */
  209.     protected function processTags($phpcsFile$stackPtr$commentStart)
  210.     {
  211.         $tokens $phpcsFile->getTokens();
  212.  
  213.         if (get_class($this=== 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FileCommentSniff'{
  214.             $docBlock 'file';
  215.         else {
  216.             $docBlock 'class';
  217.         }
  218.  
  219.         $commentEnd $tokens[$commentStart]['comment_closer'];
  220.  
  221.         $foundTags = array();
  222.         $tagTokens = array();
  223.         foreach ($tokens[$commentStart]['comment_tags'as $tag{
  224.             $name $tokens[$tag]['content'];
  225.             if (isset($this->tags[$name]=== false{
  226.                 continue;
  227.             }
  228.  
  229.             if ($this->tags[$name]['allow_multiple'=== false && isset($tagTokens[$name]=== true{
  230.                 $error 'Only one %s tag is allowed in a %s comment';
  231.                 $data  = array(
  232.                           $name,
  233.                           $docBlock,
  234.                          );
  235.                 $phpcsFile->addError($error$tag'Duplicate'.ucfirst(substr($name1)).'Tag'$data);
  236.             }
  237.  
  238.             $foundTags[]        $name;
  239.             $tagTokens[$name][$tag;
  240.  
  241.             $string $phpcsFile->findNext(T_DOC_COMMENT_STRING$tag$commentEnd);
  242.             if ($string === false || $tokens[$string]['line'!== $tokens[$tag]['line']{
  243.                 $error 'Content missing for %s tag in %s comment';
  244.                 $data  = array(
  245.                           $name,
  246.                           $docBlock,
  247.                          );
  248.                 $phpcsFile->addError($error$tag'Empty'.ucfirst(substr($name1)).'Tag'$data);
  249.                 continue;
  250.             }
  251.         }//end foreach
  252.  
  253.         // Check if the tags are in the correct position.
  254.         $pos = 0;
  255.         foreach ($this->tags as $tag => $tagData{
  256.             if (isset($tagTokens[$tag]=== false{
  257.                 if ($tagData['required'=== true{
  258.                     $error 'Missing %s tag in %s comment';
  259.                     $data  = array(
  260.                               $tag,
  261.                               $docBlock,
  262.                              );
  263.                     $phpcsFile->addError($error$commentEnd'Missing'.ucfirst(substr($tag1)).'Tag'$data);
  264.                 }
  265.  
  266.                 continue;
  267.             else {
  268.                 $method 'process'.substr($tag1);
  269.                 if (method_exists($this$method=== true{
  270.                     // Process each tag if a method is defined.
  271.                     call_user_func(array($this$method)$phpcsFile$tagTokens[$tag]);
  272.                 }
  273.             }
  274.  
  275.             if (isset($foundTags[$pos]=== false{
  276.                 break;
  277.             }
  278.  
  279.             if ($foundTags[$pos!== $tag{
  280.                 $error 'The tag in position %s should be the %s tag';
  281.                 $data  = array(
  282.                           ($pos + 1),
  283.                           $tag,
  284.                          );
  285.                 $phpcsFile->addError($error$tokens[$commentStart]['comment_tags'][$pos]ucfirst(substr($tag1)).'TagOrder'$data);
  286.             }
  287.  
  288.             // Account for multiple tags.
  289.             $pos++;
  290.             while (isset($foundTags[$pos]=== true && $foundTags[$pos=== $tag{
  291.                 $pos++;
  292.             }
  293.         }//end foreach
  294.  
  295.     }//end processTags()
  296.  
  297.  
  298.     /**
  299.      * Process the category tag.
  300.      *
  301.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  302.      * @param array                       $tags      The tokens for these tags.
  303.      *
  304.      * @return void 
  305.      */
  306.     protected function processCategory($phpcsFilearray $tags)
  307.     {
  308.         $tokens $phpcsFile->getTokens();
  309.         foreach ($tags as $tag{
  310.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  311.                 // No content.
  312.                 continue;
  313.             }
  314.  
  315.             $content $tokens[($tag + 2)]['content'];
  316.             if (Common::isUnderscoreName($content!== true{
  317.                 $newContent str_replace(' ''_'$content);
  318.                 $nameBits   explode('_'$newContent);
  319.                 $firstBit   array_shift($nameBits);
  320.                 $newName    ucfirst($firstBit).'_';
  321.                 foreach ($nameBits as $bit{
  322.                     if ($bit !== ''{
  323.                         $newName .= ucfirst($bit).'_';
  324.                     }
  325.                 }
  326.  
  327.                 $error     'Category name "%s" is not valid; consider "%s" instead';
  328.                 $validName trim($newName'_');
  329.                 $data      = array(
  330.                               $content,
  331.                               $validName,
  332.                              );
  333.                 $phpcsFile->addError($error$tag'InvalidCategory'$data);
  334.             }
  335.         }//end foreach
  336.  
  337.     }//end processCategory()
  338.  
  339.  
  340.     /**
  341.      * Process the package tag.
  342.      *
  343.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  344.      * @param array                       $tags      The tokens for these tags.
  345.      *
  346.      * @return void 
  347.      */
  348.     protected function processPackage($phpcsFilearray $tags)
  349.     {
  350.         $tokens $phpcsFile->getTokens();
  351.         foreach ($tags as $tag{
  352.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  353.                 // No content.
  354.                 continue;
  355.             }
  356.  
  357.             $content $tokens[($tag + 2)]['content'];
  358.             if (Common::isUnderscoreName($content=== true{
  359.                 continue;
  360.             }
  361.  
  362.             $newContent str_replace(' ''_'$content);
  363.             $newContent trim($newContent'_');
  364.             $newContent preg_replace('/[^A-Za-z_]/'''$newContent);
  365.  
  366.             if ($newContent === ''{
  367.                 $error 'Package name "%s" is not valid';
  368.                 $data  = array($content);
  369.                 $phpcsFile->addError($error$tag'InvalidPackageValue'$data);
  370.             else {
  371.                 $nameBits explode('_'$newContent);
  372.                 $firstBit array_shift($nameBits);
  373.                 $newName  strtoupper($firstBit{0}).substr($firstBit1).'_';
  374.                 foreach ($nameBits as $bit{
  375.                     if ($bit !== ''{
  376.                         $newName .= strtoupper($bit{0}).substr($bit1).'_';
  377.                     }
  378.                 }
  379.  
  380.                 $error     'Package name "%s" is not valid; consider "%s" instead';
  381.                 $validName trim($newName'_');
  382.                 $data      = array(
  383.                               $content,
  384.                               $validName,
  385.                              );
  386.                 $phpcsFile->addError($error$tag'InvalidPackage'$data);
  387.             }//end if
  388.         }//end foreach
  389.  
  390.     }//end processPackage()
  391.  
  392.  
  393.     /**
  394.      * Process the subpackage tag.
  395.      *
  396.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  397.      * @param array                       $tags      The tokens for these tags.
  398.      *
  399.      * @return void 
  400.      */
  401.     protected function processSubpackage($phpcsFilearray $tags)
  402.     {
  403.         $tokens $phpcsFile->getTokens();
  404.         foreach ($tags as $tag{
  405.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  406.                 // No content.
  407.                 continue;
  408.             }
  409.  
  410.             $content $tokens[($tag + 2)]['content'];
  411.             if (Common::isUnderscoreName($content=== true{
  412.                 continue;
  413.             }
  414.  
  415.             $newContent str_replace(' ''_'$content);
  416.             $nameBits   explode('_'$newContent);
  417.             $firstBit   array_shift($nameBits);
  418.             $newName    strtoupper($firstBit{0}).substr($firstBit1).'_';
  419.             foreach ($nameBits as $bit{
  420.                 if ($bit !== ''{
  421.                     $newName .= strtoupper($bit{0}).substr($bit1).'_';
  422.                 }
  423.             }
  424.  
  425.             $error     'Subpackage name "%s" is not valid; consider "%s" instead';
  426.             $validName trim($newName'_');
  427.             $data      = array(
  428.                           $content,
  429.                           $validName,
  430.                          );
  431.             $phpcsFile->addError($error$tag'InvalidSubpackage'$data);
  432.         }//end foreach
  433.  
  434.     }//end processSubpackage()
  435.  
  436.  
  437.     /**
  438.      * Process the author tag(s) that this header comment has.
  439.      *
  440.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  441.      * @param array                       $tags      The tokens for these tags.
  442.      *
  443.      * @return void 
  444.      */
  445.     protected function processAuthor($phpcsFilearray $tags)
  446.     {
  447.         $tokens $phpcsFile->getTokens();
  448.         foreach ($tags as $tag{
  449.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  450.                 // No content.
  451.                 continue;
  452.             }
  453.  
  454.             $content $tokens[($tag + 2)]['content'];
  455.             $local   '\da-zA-Z-_+';
  456.             // Dot character cannot be the first or last character in the local-part.
  457.             $localMiddle $local.'.\w';
  458.             if (preg_match('/^([^<]*)\s+<(['.$local.'](['.$localMiddle.']*['.$local.'])*@[\da-zA-Z][-.\w]*[\da-zA-Z]\.[a-zA-Z]{2,7})>$/'$content=== 0{
  459.                 $error 'Content of the @author tag must be in the form "Display Name <username@example.com>"';
  460.                 $phpcsFile->addError($error$tag'InvalidAuthors');
  461.             }
  462.         }
  463.  
  464.     }//end processAuthor()
  465.  
  466.  
  467.     /**
  468.      * Process the copyright tags.
  469.      *
  470.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  471.      * @param array                       $tags      The tokens for these tags.
  472.      *
  473.      * @return void 
  474.      */
  475.     protected function processCopyright($phpcsFilearray $tags)
  476.     {
  477.         $tokens $phpcsFile->getTokens();
  478.         foreach ($tags as $tag{
  479.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  480.                 // No content.
  481.                 continue;
  482.             }
  483.  
  484.             $content $tokens[($tag + 2)]['content'];
  485.             $matches = array();
  486.             if (preg_match('/^([0-9]{4})((.{1})([0-9]{4}))? (.+)$/'$content$matches!== 0{
  487.                 // Check earliest-latest year order.
  488.                 if ($matches[3!== '' && $matches[3!== null{
  489.                     if ($matches[3!== '-'{
  490.                         $error 'A hyphen must be used between the earliest and latest year';
  491.                         $phpcsFile->addError($error$tag'CopyrightHyphen');
  492.                     }
  493.  
  494.                     if ($matches[4!== '' && $matches[4!== null && $matches[4$matches[1]{
  495.                         $error = "Invalid year span \"$matches[1]$matches[3]$matches[4]\" found; consider \"$matches[4]-$matches[1]\" instead";
  496.                         $phpcsFile->addWarning($error$tag'InvalidCopyright');
  497.                     }
  498.                 }
  499.             else {
  500.                 $error '@copyright tag must contain a year and the name of the copyright holder';
  501.                 $phpcsFile->addError($error$tag'IncompleteCopyright');
  502.             }
  503.         }//end foreach
  504.  
  505.     }//end processCopyright()
  506.  
  507.  
  508.     /**
  509.      * Process the license tag.
  510.      *
  511.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  512.      * @param array                       $tags      The tokens for these tags.
  513.      *
  514.      * @return void 
  515.      */
  516.     protected function processLicense($phpcsFilearray $tags)
  517.     {
  518.         $tokens $phpcsFile->getTokens();
  519.         foreach ($tags as $tag{
  520.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  521.                 // No content.
  522.                 continue;
  523.             }
  524.  
  525.             $content $tokens[($tag + 2)]['content'];
  526.             $matches = array();
  527.             preg_match('/^([^\s]+)\s+(.*)/'$content$matches);
  528.             if (count($matches!== 3{
  529.                 $error '@license tag must contain a URL and a license name';
  530.                 $phpcsFile->addError($error$tag'IncompleteLicense');
  531.             }
  532.         }
  533.  
  534.     }//end processLicense()
  535.  
  536.  
  537.     /**
  538.      * Process the version tag.
  539.      *
  540.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  541.      * @param array                       $tags      The tokens for these tags.
  542.      *
  543.      * @return void 
  544.      */
  545.     protected function processVersion($phpcsFilearray $tags)
  546.     {
  547.         $tokens $phpcsFile->getTokens();
  548.         foreach ($tags as $tag{
  549.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  550.                 // No content.
  551.                 continue;
  552.             }
  553.  
  554.             $content $tokens[($tag + 2)]['content'];
  555.             if (strstr($content'CVS:'=== false
  556.                 && strstr($content'SVN:'=== false
  557.                 && strstr($content'GIT:'=== false
  558.                 && strstr($content'HG:'=== false
  559.             {
  560.                 $error 'Invalid version "%s" in file comment; consider "CVS: <cvs_id>" or "SVN: <svn_id>" or "GIT: <git_id>" or "HG: <hg_id>" instead';
  561.                 $data  = array($content);
  562.                 $phpcsFile->addWarning($error$tag'InvalidVersion'$data);
  563.             }
  564.         }
  565.  
  566.     }//end processVersion()
  567.  
  568.  
  569. }//end class

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