1<?php 2/** 3 * Squiz_Sniffs_Commenting_BlockCommentSniff. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @author Marc McIntyre <mmcintyre@squiz.net> 11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 * @link http://pear.php.net/package/PHP_CodeSniffer 14 */ 15 16/** 17 * Squiz_Sniffs_Commenting_BlockCommentSniff. 18 * 19 * Verifies that block comments are used appropriately. 20 * 21 * @category PHP 22 * @package PHP_CodeSniffer 23 * @author Greg Sherwood <gsherwood@squiz.net> 24 * @author Marc McIntyre <mmcintyre@squiz.net> 25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 26 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 27 * @version Release: @package_version@ 28 * @link http://pear.php.net/package/PHP_CodeSniffer 29 */ 30class Squiz_Sniffs_Commenting_BlockCommentSniff implements PHP_CodeSniffer_Sniff 31{ 32 33 /** 34 * The --tab-width CLI value that is being used. 35 * 36 * @var int 37 */ 38 private $_tabWidth = null; 39 40 41 /** 42 * Returns an array of tokens this test wants to listen for. 43 * 44 * @return array 45 */ 46 public function register() 47 { 48 return array( 49 T_COMMENT, 50 T_DOC_COMMENT_OPEN_TAG, 51 ); 52 53 }//end register() 54 55 56 /** 57 * Processes this test, when one of its tokens is encountered. 58 * 59 * @param PHP_CodeSniffer_File $phpcsFile The current file being scanned. 60 * @param int $stackPtr The position of the current token in the 61 * stack passed in $tokens. 62 * 63 * @return void 64 */ 65 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 66 { 67 if ($this->_tabWidth === null) { 68 $cliValues = $phpcsFile->phpcs->cli->getCommandLineValues(); 69 if (isset($cliValues['tabWidth']) === false || $cliValues['tabWidth'] === 0) { 70 // We have no idea how wide tabs are, so assume 4 spaces for fixing. 71 $this->_tabWidth = 4; 72 } else { 73 $this->_tabWidth = $cliValues['tabWidth']; 74 } 75 } 76 77 $tokens = $phpcsFile->getTokens(); 78 79 // If it's an inline comment, return. 80 if (substr($tokens[$stackPtr]['content'], 0, 2) !== '/*') { 81 return; 82 } 83 84 // If this is a function/class/interface doc block comment, skip it. 85 // We are only interested in inline doc block comments. 86 if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { 87 $nextToken = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), null, true); 88 $ignore = array( 89 T_CLASS => true, 90 T_INTERFACE => true, 91 T_TRAIT => true, 92 T_FUNCTION => true, 93 T_PUBLIC => true, 94 T_PRIVATE => true, 95 T_FINAL => true, 96 T_PROTECTED => true, 97 T_STATIC => true, 98 T_ABSTRACT => true, 99 T_CONST => true, 100 T_VAR => true, 101 ); 102 if (isset($ignore[$tokens[$nextToken]['code']]) === true) { 103 return; 104 } 105 106 $prevToken = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); 107 if ($tokens[$prevToken]['code'] === T_OPEN_TAG) { 108 return; 109 } 110 111 $error = 'Block comments must be started with /*'; 112 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStart'); 113 if ($fix === true) { 114 $phpcsFile->fixer->replaceToken($stackPtr, '/*'); 115 } 116 117 $end = $tokens[$stackPtr]['comment_closer']; 118 if ($tokens[$end]['content'] !== '*/') { 119 $error = 'Block comments must be ended with */'; 120 $fix = $phpcsFile->addFixableError($error, $end, 'WrongEnd'); 121 if ($fix === true) { 122 $phpcsFile->fixer->replaceToken($end, '*/'); 123 } 124 } 125 126 return; 127 }//end if 128 129 $commentLines = array($stackPtr); 130 $nextComment = $stackPtr; 131 $lastLine = $tokens[$stackPtr]['line']; 132 $commentString = $tokens[$stackPtr]['content']; 133 134 // Construct the comment into an array. 135 while (($nextComment = $phpcsFile->findNext(T_WHITESPACE, ($nextComment + 1), null, true)) !== false) { 136 if ($tokens[$nextComment]['code'] !== $tokens[$stackPtr]['code']) { 137 // Found the next bit of code. 138 break; 139 } 140 141 if (($tokens[$nextComment]['line'] - 1) !== $lastLine) { 142 // Not part of the block. 143 break; 144 } 145 146 $lastLine = $tokens[$nextComment]['line']; 147 $commentLines[] = $nextComment; 148 $commentString .= $tokens[$nextComment]['content']; 149 if ($tokens[$nextComment]['code'] === T_DOC_COMMENT_CLOSE_TAG) { 150 break; 151 } 152 } 153 154 $commentText = str_replace($phpcsFile->eolChar, '', $commentString); 155 $commentText = trim($commentText, '/* '); 156 if ($commentText === '') { 157 $error = 'Empty block comment not allowed'; 158 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Empty'); 159 if ($fix === true) { 160 $phpcsFile->fixer->beginChangeset(); 161 $phpcsFile->fixer->replaceToken($stackPtr, ''); 162 $lastToken = array_pop($commentLines); 163 for ($i = ($stackPtr + 1); $i <= $lastToken; $i++) { 164 $phpcsFile->fixer->replaceToken($i, ''); 165 } 166 167 $phpcsFile->fixer->endChangeset(); 168 } 169 170 return; 171 } 172 173 if (count($commentLines) === 1) { 174 $error = 'Single line block comment not allowed; use inline ("// text") comment instead'; 175 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SingleLine'); 176 if ($fix === true) { 177 $comment = '// '.$commentText.$phpcsFile->eolChar; 178 $phpcsFile->fixer->replaceToken($stackPtr, $comment); 179 } 180 181 return; 182 } 183 184 $content = trim($tokens[$stackPtr]['content']); 185 if ($content !== '/*' && $content !== '/**') { 186 $error = 'Block comment text must start on a new line'; 187 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoNewLine'); 188 if ($fix === true) { 189 $indent = ''; 190 if ($tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) { 191 if (isset($tokens[($stackPtr - 1)]['orig_content']) === true) { 192 $indent = $tokens[($stackPtr - 1)]['orig_content']; 193 } else { 194 $indent = $tokens[($stackPtr - 1)]['content']; 195 } 196 } 197 198 $comment = preg_replace( 199 '/^(\s*\/\*\*?)/', 200 '$1'.$phpcsFile->eolChar.$indent, 201 $tokens[$stackPtr]['content'], 202 1 203 ); 204 $phpcsFile->fixer->replaceToken($stackPtr, $comment); 205 } 206 207 return; 208 }//end if 209 210 $starColumn = ($tokens[$stackPtr]['column'] + 3); 211 212 // Make sure first line isn't blank. 213 if (trim($tokens[$commentLines[1]]['content']) === '') { 214 $error = 'Empty line not allowed at start of comment'; 215 $fix = $phpcsFile->addFixableError($error, $commentLines[1], 'HasEmptyLine'); 216 if ($fix === true) { 217 $phpcsFile->fixer->replaceToken($commentLines[1], ''); 218 } 219 } else { 220 // Check indentation of first line. 221 $content = $tokens[$commentLines[1]]['content']; 222 $commentText = ltrim($content); 223 $leadingSpace = (strlen($content) - strlen($commentText)); 224 if ($leadingSpace !== $starColumn) { 225 $expected = $starColumn.' space'; 226 if ($starColumn !== 1) { 227 $expected .= 's'; 228 } 229 230 $data = array( 231 $expected, 232 $leadingSpace, 233 ); 234 235 $error = 'First line of comment not aligned correctly; expected %s but found %s'; 236 $fix = $phpcsFile->addFixableError($error, $commentLines[1], 'FirstLineIndent', $data); 237 if ($fix === true) { 238 if (isset($tokens[$commentLines[1]]['orig_content']) === true 239 && $tokens[$commentLines[1]]['orig_content'][0] === "\t" 240 ) { 241 // Line is indented using tabs. 242 $padding = str_repeat("\t", floor($starColumn / $this->_tabWidth)); 243 } else { 244 $padding = str_repeat(' ', $starColumn); 245 } 246 247 $phpcsFile->fixer->replaceToken($commentLines[1], $padding.ltrim($content)); 248 } 249 }//end if 250 251 if (preg_match('/^\p{Ll}/u', $commentText) === 1) { 252 $error = 'Block comments must start with a capital letter'; 253 $phpcsFile->addError($error, $commentLines[1], 'NoCapital'); 254 } 255 }//end if 256 257 // Check that each line of the comment is indented past the star. 258 foreach ($commentLines as $line) { 259 $leadingSpace = (strlen($tokens[$line]['content']) - strlen(ltrim($tokens[$line]['content']))); 260 // First and last lines (comment opener and closer) are handled separately. 261 if ($line === $commentLines[(count($commentLines) - 1)] || $line === $commentLines[0]) { 262 continue; 263 } 264 265 // First comment line was handled above. 266 if ($line === $commentLines[1]) { 267 continue; 268 } 269 270 // If it's empty, continue. 271 if (trim($tokens[$line]['content']) === '') { 272 continue; 273 } 274 275 if ($leadingSpace < $starColumn) { 276 $expected = $starColumn.' space'; 277 if ($starColumn !== 1) { 278 $expected .= 's'; 279 } 280 281 $data = array( 282 $expected, 283 $leadingSpace, 284 ); 285 286 $error = 'Comment line indented incorrectly; expected at least %s but found %s'; 287 $fix = $phpcsFile->addFixableError($error, $line, 'LineIndent', $data); 288 if ($fix === true) { 289 if (isset($tokens[$line]['orig_content']) === true 290 && $tokens[$line]['orig_content'][0] === "\t" 291 ) { 292 // Line is indented using tabs. 293 $padding = str_repeat("\t", floor($starColumn / $this->_tabWidth)); 294 } else { 295 $padding = str_repeat(' ', $starColumn); 296 } 297 298 $phpcsFile->fixer->replaceToken($line, $padding.ltrim($tokens[$line]['content'])); 299 } 300 }//end if 301 }//end foreach 302 303 // Finally, test the last line is correct. 304 $lastIndex = (count($commentLines) - 1); 305 $content = trim($tokens[$commentLines[$lastIndex]]['content']); 306 if ($content !== '*/' && $content !== '**/') { 307 $error = 'Comment closer must be on a new line'; 308 $phpcsFile->addError($error, $commentLines[$lastIndex], 'CloserSameLine'); 309 } else { 310 $content = $tokens[$commentLines[$lastIndex]]['content']; 311 $commentText = ltrim($content); 312 $leadingSpace = (strlen($content) - strlen($commentText)); 313 if ($leadingSpace !== ($tokens[$stackPtr]['column'] - 1)) { 314 $expected = ($tokens[$stackPtr]['column'] - 1); 315 if ($expected === 1) { 316 $expected .= ' space'; 317 } else { 318 $expected .= ' spaces'; 319 } 320 321 $data = array( 322 $expected, 323 $leadingSpace, 324 ); 325 326 $error = 'Last line of comment aligned incorrectly; expected %s but found %s'; 327 $phpcsFile->addError($error, $commentLines[$lastIndex], 'LastLineIndent', $data); 328 } 329 }//end if 330 331 // Check that the lines before and after this comment are blank. 332 $contentBefore = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); 333 if (isset($tokens[$contentBefore]['scope_closer']) === true 334 && $tokens[$contentBefore]['scope_opener'] === $contentBefore 335 ) { 336 if (($tokens[$stackPtr]['line'] - $tokens[$contentBefore]['line']) !== 1) { 337 $error = 'Empty line not required before block comment'; 338 $phpcsFile->addError($error, $stackPtr, 'HasEmptyLineBefore'); 339 } 340 } else { 341 if (($tokens[$stackPtr]['line'] - $tokens[$contentBefore]['line']) < 2) { 342 $error = 'Empty line required before block comment'; 343 $phpcsFile->addError($error, $stackPtr, 'NoEmptyLineBefore'); 344 } 345 } 346 347 $commentCloser = $commentLines[$lastIndex]; 348 $contentAfter = $phpcsFile->findNext(T_WHITESPACE, ($commentCloser + 1), null, true); 349 if ($contentAfter !== false && ($tokens[$contentAfter]['line'] - $tokens[$commentCloser]['line']) < 2) { 350 $error = 'Empty line required after block comment'; 351 $phpcsFile->addError($error, $commentCloser, 'NoEmptyLineAfter'); 352 } 353 354 }//end process() 355 356 357}//end class 358