1<?php 2/** 3 * PSR1_Sniffs_Files_SideEffectsSniff. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 11 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 12 * @link http://pear.php.net/package/PHP_CodeSniffer 13 */ 14 15/** 16 * PSR1_Sniffs_Files_SideEffectsSniff. 17 * 18 * Ensures a file declare new symbols and causes no other side effects, or executes 19 * logic with side effects, but not both. 20 * 21 * @category PHP 22 * @package PHP_CodeSniffer 23 * @author Greg Sherwood <gsherwood@squiz.net> 24 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 25 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 26 * @version Release: @package_version@ 27 * @link http://pear.php.net/package/PHP_CodeSniffer 28 */ 29class PSR1_Sniffs_Files_SideEffectsSniff implements PHP_CodeSniffer_Sniff 30{ 31 32 33 /** 34 * Returns an array of tokens this test wants to listen for. 35 * 36 * @return array 37 */ 38 public function register() 39 { 40 return array(T_OPEN_TAG); 41 42 }//end register() 43 44 45 /** 46 * Processes this sniff, when one of its tokens is encountered. 47 * 48 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 49 * @param int $stackPtr The position of the current token in 50 * the token stack. 51 * 52 * @return void 53 */ 54 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 55 { 56 $tokens = $phpcsFile->getTokens(); 57 $result = $this->_searchForConflict($phpcsFile, 0, ($phpcsFile->numTokens - 1), $tokens); 58 59 if ($result['symbol'] !== null && $result['effect'] !== null) { 60 $error = 'A file should declare new symbols (classes, functions, constants, etc.) and cause no other side effects, or it should execute logic with side effects, but should not do both. The first symbol is defined on line %s and the first side effect is on line %s.'; 61 $data = array( 62 $tokens[$result['symbol']]['line'], 63 $tokens[$result['effect']]['line'], 64 ); 65 $phpcsFile->addWarning($error, 0, 'FoundWithSymbols', $data); 66 $phpcsFile->recordMetric($stackPtr, 'Declarations and side effects mixed', 'yes'); 67 } else { 68 $phpcsFile->recordMetric($stackPtr, 'Declarations and side effects mixed', 'no'); 69 } 70 71 // Ignore the rest of the file. 72 return ($phpcsFile->numTokens + 1); 73 74 }//end process() 75 76 77 /** 78 * Searches for symbol declarations and side effects. 79 * 80 * Returns the positions of both the first symbol declared and the first 81 * side effect in the file. A NULL value for either indicates nothing was 82 * found. 83 * 84 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 85 * @param int $start The token to start searching from. 86 * @param int $end The token to search to. 87 * @param array $tokens The stack of tokens that make up 88 * the file. 89 * 90 * @return array 91 */ 92 private function _searchForConflict(PHP_CodeSniffer_File $phpcsFile, $start, $end, $tokens) 93 { 94 $symbols = array( 95 T_CLASS => T_CLASS, 96 T_INTERFACE => T_INTERFACE, 97 T_TRAIT => T_TRAIT, 98 T_FUNCTION => T_FUNCTION, 99 ); 100 101 $conditions = array( 102 T_IF => T_IF, 103 T_ELSE => T_ELSE, 104 T_ELSEIF => T_ELSEIF, 105 ); 106 107 $firstSymbol = null; 108 $firstEffect = null; 109 for ($i = $start; $i <= $end; $i++) { 110 // Ignore whitespace and comments. 111 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { 112 continue; 113 } 114 115 // Ignore PHP tags. 116 if ($tokens[$i]['code'] === T_OPEN_TAG 117 || $tokens[$i]['code'] === T_CLOSE_TAG 118 ) { 119 continue; 120 } 121 122 // Ignore shebang. 123 if (substr($tokens[$i]['content'], 0, 2) === '#!') { 124 continue; 125 } 126 127 // Ignore entire namespace, declare, const and use statements. 128 if ($tokens[$i]['code'] === T_NAMESPACE 129 || $tokens[$i]['code'] === T_USE 130 || $tokens[$i]['code'] === T_DECLARE 131 || $tokens[$i]['code'] === T_CONST 132 ) { 133 if (isset($tokens[$i]['scope_opener']) === true) { 134 $i = $tokens[$i]['scope_closer']; 135 } else { 136 $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($i + 1)); 137 if ($semicolon !== false) { 138 $i = $semicolon; 139 } 140 } 141 142 continue; 143 } 144 145 // Ignore function/class prefixes. 146 if (isset(PHP_CodeSniffer_Tokens::$methodPrefixes[$tokens[$i]['code']]) === true) { 147 continue; 148 } 149 150 // Ignore anon classes. 151 if ($tokens[$i]['code'] === T_ANON_CLASS) { 152 $i = $tokens[$i]['scope_closer']; 153 continue; 154 } 155 156 // Detect and skip over symbols. 157 if (isset($symbols[$tokens[$i]['code']]) === true 158 && isset($tokens[$i]['scope_closer']) === true 159 ) { 160 if ($firstSymbol === null) { 161 $firstSymbol = $i; 162 } 163 164 $i = $tokens[$i]['scope_closer']; 165 continue; 166 } else if ($tokens[$i]['code'] === T_STRING 167 && strtolower($tokens[$i]['content']) === 'define' 168 ) { 169 $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($i - 1), null, true); 170 if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR) { 171 if ($firstSymbol === null) { 172 $firstSymbol = $i; 173 } 174 175 $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($i + 1)); 176 if ($semicolon !== false) { 177 $i = $semicolon; 178 } 179 180 continue; 181 } 182 }//end if 183 184 // Conditional statements are allowed in symbol files as long as the 185 // contents is only a symbol definition. So don't count these as effects 186 // in this case. 187 if (isset($conditions[$tokens[$i]['code']]) === true) { 188 if (isset($tokens[$i]['scope_opener']) === false) { 189 // Probably an "else if", so just ignore. 190 continue; 191 } 192 193 $result = $this->_searchForConflict( 194 $phpcsFile, 195 ($tokens[$i]['scope_opener'] + 1), 196 ($tokens[$i]['scope_closer'] - 1), 197 $tokens 198 ); 199 200 if ($result['symbol'] !== null) { 201 if ($firstSymbol === null) { 202 $firstSymbol = $result['symbol']; 203 } 204 205 if ($result['effect'] !== null) { 206 // Found a conflict. 207 $firstEffect = $result['effect']; 208 break; 209 } 210 } 211 212 if ($firstEffect === null) { 213 $firstEffect = $result['effect']; 214 } 215 216 $i = $tokens[$i]['scope_closer']; 217 continue; 218 }//end if 219 220 if ($firstEffect === null) { 221 $firstEffect = $i; 222 } 223 224 if ($firstSymbol !== null) { 225 // We have a conflict we have to report, so no point continuing. 226 break; 227 } 228 }//end for 229 230 return array( 231 'symbol' => $firstSymbol, 232 'effect' => $firstEffect, 233 ); 234 235 }//end _searchForConflict() 236 237 238}//end class 239