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

Source for file SwitchDeclarationSniff.php

Documentation is available at SwitchDeclarationSniff.php

  1. <?php
  2. /**
  3.  * Ensures all switch statements are defined correctly.
  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\PSR2\Sniffs\ControlStructures;
  11.  
  12. use PHP_CodeSniffer\Sniffs\Sniff;
  13. use PHP_CodeSniffer\Files\File;
  14. use PHP_CodeSniffer\Util\Tokens;
  15.  
  16. class SwitchDeclarationSniff implements Sniff
  17. {
  18.  
  19.     /**
  20.      * The number of spaces code should be indented.
  21.      *
  22.      * @var integer 
  23.      */
  24.     public $indent = 4;
  25.  
  26.  
  27.     /**
  28.      * Returns an array of tokens this test wants to listen for.
  29.      *
  30.      * @return array 
  31.      */
  32.     public function register()
  33.     {
  34.         return array(T_SWITCH);
  35.  
  36.     }//end register()
  37.  
  38.  
  39.     /**
  40.      * Processes this test, when one of its tokens is encountered.
  41.      *
  42.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  43.      * @param int                         $stackPtr  The position of the current token in the
  44.      *                                                stack passed in $tokens.
  45.      *
  46.      * @return void 
  47.      */
  48.     public function process(File $phpcsFile$stackPtr)
  49.     {
  50.         $tokens $phpcsFile->getTokens();
  51.  
  52.         // We can't process SWITCH statements unless we know where they start and end.
  53.         if (isset($tokens[$stackPtr]['scope_opener']=== false
  54.             || isset($tokens[$stackPtr]['scope_closer']=== false
  55.         {
  56.             return;
  57.         }
  58.  
  59.         $switch        $tokens[$stackPtr];
  60.         $nextCase      $stackPtr;
  61.         $caseAlignment ($switch['column'$this->indent);
  62.         $caseCount     = 0;
  63.         $foundDefault  = false;
  64.  
  65.         while (($nextCase $this->findNextCase($phpcsFile($nextCase + 1)$switch['scope_closer'])) !== false{
  66.             if ($tokens[$nextCase]['code'=== T_DEFAULT{
  67.                 $type         'default';
  68.                 $foundDefault = true;
  69.             else {
  70.                 $type 'case';
  71.                 $caseCount++;
  72.             }
  73.  
  74.             if ($tokens[$nextCase]['content'!== strtolower($tokens[$nextCase]['content'])) {
  75.                 $expected strtolower($tokens[$nextCase]['content']);
  76.                 $error    strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"';
  77.                 $data     = array(
  78.                              $expected,
  79.                              $tokens[$nextCase]['content'],
  80.                             );
  81.  
  82.                 $fix $phpcsFile->addFixableError($error$nextCase$type.'NotLower'$data);
  83.                 if ($fix === true{
  84.                     $phpcsFile->fixer->replaceToken($nextCase$expected);
  85.                 }
  86.             }
  87.  
  88.             if ($type === 'case'
  89.                 && ($tokens[($nextCase + 1)]['code'!== T_WHITESPACE
  90.                 || $tokens[($nextCase + 1)]['content'!== ' ')
  91.             {
  92.                 $error 'CASE keyword must be followed by a single space';
  93.                 $fix   $phpcsFile->addFixableError($error$nextCase'SpacingAfterCase');
  94.                 if ($fix === true{
  95.                     if ($tokens[($nextCase + 1)]['code'!== T_WHITESPACE{
  96.                         $phpcsFile->fixer->addContent($nextCase' ');
  97.                     else {
  98.                         $phpcsFile->fixer->replaceToken(($nextCase + 1)' ');
  99.                     }
  100.                 }
  101.             }
  102.  
  103.             $opener     $tokens[$nextCase]['scope_opener'];
  104.             $nextCloser $tokens[$nextCase]['scope_closer'];
  105.             if ($tokens[$opener]['code'=== T_COLON{
  106.                 if ($tokens[($opener - 1)]['code'=== T_WHITESPACE{
  107.                     $error 'There must be no space before the colon in a '.strtoupper($type).' statement';
  108.                     $fix   $phpcsFile->addFixableError($error$nextCase'SpaceBeforeColon'.strtoupper($type));
  109.                     if ($fix === true{
  110.                         $phpcsFile->fixer->replaceToken(($opener - 1)'');
  111.                     }
  112.                 }
  113.  
  114.                 $next $phpcsFile->findNext(T_WHITESPACE($opener + 1)nulltrue);
  115.                 if ($tokens[$next]['line'=== $tokens[$opener]['line']
  116.                     && $tokens[$next]['code'=== T_COMMENT
  117.                 {
  118.                     // Skip comments on the same line.
  119.                     $next $phpcsFile->findNext(T_WHITESPACE($next + 1)nulltrue);
  120.                 }
  121.  
  122.                 if ($tokens[$next]['line'!== ($tokens[$opener]['line'+ 1)) {
  123.                     $error 'The '.strtoupper($type).' body must start on the line following the statement';
  124.                     $fix   $phpcsFile->addFixableError($error$nextCase'BodyOnNextLine'.strtoupper($type));
  125.                     if ($fix === true{
  126.                         if ($tokens[$next]['line'=== $tokens[$opener]['line']{
  127.                             $padding str_repeat(' '($caseAlignment $this->indent - 1));
  128.                             $phpcsFile->fixer->addContentBefore($next$phpcsFile->eolChar.$padding);
  129.                         else {
  130.                             $phpcsFile->fixer->beginChangeset();
  131.                             for ($i ($opener + 1)$i $next$i++{
  132.                                 if ($tokens[$i]['line'=== $tokens[$next]['line']{
  133.                                     break;
  134.                                 }
  135.  
  136.                                 $phpcsFile->fixer->replaceToken($i'');
  137.                             }
  138.  
  139.                             $phpcsFile->fixer->addNewLineBefore($i);
  140.                             $phpcsFile->fixer->endChangeset();
  141.                         }
  142.                     }
  143.                 }//end if
  144.  
  145.                 if ($tokens[$nextCloser]['scope_condition'=== $nextCase{
  146.                     // Only need to check some things once, even if the
  147.                     // closer is shared between multiple case statements, or even
  148.                     // the default case.
  149.                     $prev $phpcsFile->findPrevious(T_WHITESPACE($nextCloser - 1)$nextCasetrue);
  150.                     if ($tokens[$prev]['line'=== $tokens[$nextCloser]['line']{
  151.                         $error 'Terminating statement must be on a line by itself';
  152.                         $fix   $phpcsFile->addFixableError($error$nextCloser'BreakNotNewLine');
  153.                         if ($fix === true{
  154.                             $phpcsFile->fixer->addNewLine($prev);
  155.                             $phpcsFile->fixer->replaceToken($nextClosertrim($tokens[$nextCloser]['content']));
  156.                         }
  157.                     else {
  158.                         $diff ($caseAlignment $this->indent $tokens[$nextCloser]['column']);
  159.                         if ($diff !== 0{
  160.                             $error 'Terminating statement must be indented to the same level as the CASE body';
  161.                             $fix   $phpcsFile->addFixableError($error$nextCloser'BreakIndent');
  162.                             if ($fix === true{
  163.                                 if ($diff > 0{
  164.                                     $phpcsFile->fixer->addContentBefore($nextCloserstr_repeat(' '$diff));
  165.                                 else {
  166.                                     $phpcsFile->fixer->substrToken(($nextCloser - 1)0$diff);
  167.                                 }
  168.                             }
  169.                         }
  170.                     }//end if
  171.                 }//end if
  172.             else {
  173.                 $error strtoupper($type).' statements must be defined using a colon';
  174.                 $phpcsFile->addError($error$nextCase'WrongOpener'.$type);
  175.             }//end if
  176.  
  177.             // We only want cases from here on in.
  178.             if ($type !== 'case'{
  179.                 continue;
  180.             }
  181.  
  182.             $nextCode $phpcsFile->findNext(T_WHITESPACE($opener + 1)$nextClosertrue);
  183.  
  184.             if ($tokens[$nextCode]['code'!== T_CASE && $tokens[$nextCode]['code'!== T_DEFAULT{
  185.                 // This case statement has content. If the next case or default comes
  186.                 // before the closer, it means we don't have an obvious terminating
  187.                 // statement and need to make some more effort to find one. If we
  188.                 // don't, we do need a comment.
  189.                 $nextCode $this->findNextCase($phpcsFile($opener + 1)$nextCloser);
  190.                 if ($nextCode !== false{
  191.                     $prevCode $phpcsFile->findPrevious(T_WHITESPACE($nextCode - 1)$nextCasetrue);
  192.                     if ($tokens[$prevCode]['code'!== T_COMMENT
  193.                         && $this->findNestedTerminator($phpcsFile($opener + 1)$nextCode=== false
  194.                     {
  195.                         $error 'There must be a comment when fall-through is intentional in a non-empty case body';
  196.                         $phpcsFile->addError($error$nextCase'TerminatingComment');
  197.                     }
  198.                 }
  199.             }
  200.         }//end while
  201.  
  202.     }//end process()
  203.  
  204.  
  205.     /**
  206.      * Find the next CASE or DEFAULT statement from a point in the file.
  207.      *
  208.      * Note that nested switches are ignored.
  209.      *
  210.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  211.      * @param int                         $stackPtr  The position to start looking at.
  212.      * @param int                         $end       The position to stop looking at.
  213.      *
  214.      * @return int | bool
  215.      */
  216.     private function findNextCase($phpcsFile$stackPtr$end)
  217.     {
  218.         $tokens $phpcsFile->getTokens();
  219.         while (($stackPtr $phpcsFile->findNext(array(T_CASET_DEFAULTT_SWITCH)$stackPtr$end)) !== false{
  220.             // Skip nested SWITCH statements; they are handled on their own.
  221.             if ($tokens[$stackPtr]['code'=== T_SWITCH{
  222.                 $stackPtr $tokens[$stackPtr]['scope_closer'];
  223.                 continue;
  224.             }
  225.  
  226.             break;
  227.         }
  228.  
  229.         return $stackPtr;
  230.  
  231.     }//end findNextCase()
  232.  
  233.  
  234.     /**
  235.      * Returns true if a nested terminating statement is found.
  236.      *
  237.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  238.      * @param int                         $stackPtr  The position to start looking at.
  239.      * @param int                         $end       The position to stop looking at.
  240.      *
  241.      * @return bool 
  242.      */
  243.     private function findNestedTerminator($phpcsFile$stackPtr$end)
  244.     {
  245.         $tokens      $phpcsFile->getTokens();
  246.         $terminators = array(
  247.                         T_RETURN,
  248.                         T_BREAK,
  249.                         T_CONTINUE,
  250.                         T_THROW,
  251.                         T_EXIT,
  252.                        );
  253.  
  254.         $lastToken $phpcsFile->findPrevious(T_WHITESPACE($end - 1)$stackPtrtrue);
  255.         if ($lastToken !== false{
  256.             if ($tokens[$lastToken]['code'=== T_CLOSE_CURLY_BRACKET{
  257.                 // We found a closing curly bracket and want to check if its
  258.                 // block belongs to an IF, ELSEIF or ELSE clause. If yes, we
  259.                 // continue searching for a terminating statement within that
  260.                 // block. Note that we have to make sure that every block of
  261.                 // the entire if/else statement has a terminating statement.
  262.                 $currentCloser $lastToken;
  263.                 $hasElseBlock  = false;
  264.                 do {
  265.                     $scopeOpener $tokens[$currentCloser]['scope_opener'];
  266.                     $scopeCloser $tokens[$currentCloser]['scope_closer'];
  267.  
  268.                     $prevToken $phpcsFile->findPrevious(T_WHITESPACE($scopeOpener - 1)$stackPtrtrue);
  269.                     if ($prevToken === false{
  270.                         return false;
  271.                     }
  272.  
  273.                     // IF and ELSEIF clauses possess a condition we have to account for.
  274.                     if ($tokens[$prevToken]['code'=== T_CLOSE_PARENTHESIS{
  275.                         $prevToken $tokens[$prevToken]['parenthesis_owner'];
  276.                     }
  277.  
  278.                     if ($tokens[$prevToken]['code'=== T_IF{
  279.                         // If we have not encountered an ELSE clause by now, we cannot
  280.                         // be sure that the whole statement terminates in every case.
  281.                         if ($hasElseBlock === false{
  282.                             return false;
  283.                         }
  284.  
  285.                         return $this->findNestedTerminator($phpcsFile($scopeOpener + 1)$scopeCloser);
  286.                     else if ($tokens[$prevToken]['code'=== T_ELSEIF
  287.                         || $tokens[$prevToken]['code'=== T_ELSE
  288.                     {
  289.                         // If we find a terminating statement within this block,
  290.                         // we continue with the previous ELSEIF or IF clause.
  291.                         $hasTerminator $this->findNestedTerminator($phpcsFile($scopeOpener + 1)$scopeCloser);
  292.                         if ($hasTerminator === false{
  293.                             return false;
  294.                         }
  295.  
  296.                         $currentCloser $phpcsFile->findPrevious(T_WHITESPACE($prevToken - 1)$stackPtrtrue);
  297.                         if ($tokens[$prevToken]['code'=== T_ELSE{
  298.                             $hasElseBlock = true;
  299.                         }
  300.                     else {
  301.                         return false;
  302.                     }//end if
  303.                 while ($currentCloser !== false && $tokens[$currentCloser]['code'=== T_CLOSE_CURLY_BRACKET);
  304.  
  305.                 return true;
  306.             else if ($tokens[$lastToken]['code'=== T_SEMICOLON{
  307.                 // We found the last statement of the CASE. Now we want to
  308.                 // check whether it is a terminating one.
  309.                 $terminator $phpcsFile->findStartOfStatement(($lastToken - 1));
  310.                 if (in_array($tokens[$terminator]['code']$terminatorstrue=== true{
  311.                     return $terminator;
  312.                 }
  313.             }//end if
  314.         }//end if
  315.  
  316.         return false;
  317.  
  318.     }//end findNestedTerminator()
  319.  
  320.  
  321. }//end class

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