1<?php 2/** 3 * Ensures the create() method of widget types properly uses callbacks. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer_MySource 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 * Ensures the create() method of widget types properly uses callbacks. 17 * 18 * @category PHP 19 * @package PHP_CodeSniffer_MySource 20 * @author Greg Sherwood <gsherwood@squiz.net> 21 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 22 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 23 * @version Release: @package_version@ 24 * @link http://pear.php.net/package/PHP_CodeSniffer 25 */ 26class MySource_Sniffs_Objects_CreateWidgetTypeCallbackSniff implements PHP_CodeSniffer_Sniff 27{ 28 29 /** 30 * A list of tokenizers this sniff supports. 31 * 32 * @var array 33 */ 34 public $supportedTokenizers = array('JS'); 35 36 37 /** 38 * Returns an array of tokens this test wants to listen for. 39 * 40 * @return array 41 */ 42 public function register() 43 { 44 return array(T_OBJECT); 45 46 }//end register() 47 48 49 /** 50 * Processes this test, when one of its tokens is encountered. 51 * 52 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 53 * @param int $stackPtr The position of the current token 54 * in the stack passed in $tokens. 55 * 56 * @return void 57 */ 58 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 59 { 60 $tokens = $phpcsFile->getTokens(); 61 62 $className = $phpcsFile->findPrevious(T_STRING, ($stackPtr - 1)); 63 if (substr(strtolower($tokens[$className]['content']), -10) !== 'widgettype') { 64 return; 65 } 66 67 // Search for a create method. 68 $create = $phpcsFile->findNext(T_PROPERTY, $stackPtr, $tokens[$stackPtr]['bracket_closer'], null, 'create'); 69 if ($create === false) { 70 return; 71 } 72 73 $function = $phpcsFile->findNext(array(T_WHITESPACE, T_COLON), ($create + 1), null, true); 74 if ($tokens[$function]['code'] !== T_FUNCTION 75 && $tokens[$function]['code'] !== T_CLOSURE 76 ) { 77 return; 78 } 79 80 $start = ($tokens[$function]['scope_opener'] + 1); 81 $end = ($tokens[$function]['scope_closer'] - 1); 82 83 // Check that the first argument is called "callback". 84 $arg = $phpcsFile->findNext(T_WHITESPACE, ($tokens[$function]['parenthesis_opener'] + 1), null, true); 85 if ($tokens[$arg]['content'] !== 'callback') { 86 $error = 'The first argument of the create() method of a widget type must be called "callback"'; 87 $phpcsFile->addError($error, $arg, 'FirstArgNotCallback'); 88 } 89 90 /* 91 Look for return statements within the function. They cannot return 92 anything and must be preceded by the callback.call() line. The 93 callback itself must contain "self" or "this" as the first argument 94 and there needs to be a call to the callback function somewhere 95 in the create method. All calls to the callback function must be 96 followed by a return statement or the end of the method. 97 */ 98 99 $foundCallback = false; 100 $passedCallback = false; 101 $nestedFunction = null; 102 for ($i = $start; $i <= $end; $i++) { 103 // Keep track of nested functions. 104 if ($nestedFunction !== null) { 105 if ($i === $nestedFunction) { 106 $nestedFunction = null; 107 continue; 108 } 109 } else if (($tokens[$i]['code'] === T_FUNCTION 110 || $tokens[$i]['code'] === T_CLOSURE) 111 && isset($tokens[$i]['scope_closer']) === true 112 ) { 113 $nestedFunction = $tokens[$i]['scope_closer']; 114 continue; 115 } 116 117 if ($nestedFunction === null && $tokens[$i]['code'] === T_RETURN) { 118 // Make sure return statements are not returning anything. 119 if ($tokens[($i + 1)]['code'] !== T_SEMICOLON) { 120 $error = 'The create() method of a widget type must not return a value'; 121 $phpcsFile->addError($error, $i, 'ReturnValue'); 122 } 123 124 continue; 125 } else if ($tokens[$i]['code'] !== T_STRING 126 || $tokens[$i]['content'] !== 'callback' 127 ) { 128 continue; 129 } 130 131 // If this is the form "callback.call(" then it is a call 132 // to the callback function. 133 if ($tokens[($i + 1)]['code'] !== T_OBJECT_OPERATOR 134 || $tokens[($i + 2)]['content'] !== 'call' 135 || $tokens[($i + 3)]['code'] !== T_OPEN_PARENTHESIS 136 ) { 137 // One last chance; this might be the callback function 138 // being passed to another function, like this 139 // "this.init(something, callback, something)". 140 if (isset($tokens[$i]['nested_parenthesis']) === false) { 141 continue; 142 } 143 144 // Just make sure those brackets dont belong to anyone, 145 // like an IF or FOR statement. 146 foreach ($tokens[$i]['nested_parenthesis'] as $bracket) { 147 if (isset($tokens[$bracket]['parenthesis_owner']) === true) { 148 continue(2); 149 } 150 } 151 152 // Note that we use this endBracket down further when checking 153 // for a RETURN statement. 154 $endBracket = end($tokens[$i]['nested_parenthesis']); 155 $bracket = key($tokens[$i]['nested_parenthesis']); 156 157 $prev = $phpcsFile->findPrevious( 158 PHP_CodeSniffer_Tokens::$emptyTokens, 159 ($bracket - 1), 160 null, 161 true 162 ); 163 164 if ($tokens[$prev]['code'] !== T_STRING) { 165 // This is not a function passing the callback. 166 continue; 167 } 168 169 $passedCallback = true; 170 }//end if 171 172 $foundCallback = true; 173 174 if ($passedCallback === false) { 175 // The first argument must be "this" or "self". 176 $arg = $phpcsFile->findNext(T_WHITESPACE, ($i + 4), null, true); 177 if ($tokens[$arg]['content'] !== 'this' 178 && $tokens[$arg]['content'] !== 'self' 179 ) { 180 $error = 'The first argument passed to the callback function must be "this" or "self"'; 181 $phpcsFile->addError($error, $arg, 'FirstArgNotSelf'); 182 } 183 } 184 185 // Now it must be followed by a return statement or the end of the function. 186 if ($passedCallback === false) { 187 $endBracket = $tokens[($i + 3)]['parenthesis_closer']; 188 } 189 190 for ($next = $endBracket; $next <= $end; $next++) { 191 // Skip whitespace so we find the next content after the call. 192 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$next]['code']]) === true) { 193 continue; 194 } 195 196 // Skip closing braces like END IF because it is not executable code. 197 if ($tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) { 198 continue; 199 } 200 201 // We don't care about anything on the current line, like a 202 // semicolon. It doesn't matter if there are other statements on the 203 // line because another sniff will check for those. 204 if ($tokens[$next]['line'] === $tokens[$endBracket]['line']) { 205 continue; 206 } 207 208 break; 209 } 210 211 if ($next !== $tokens[$function]['scope_closer'] 212 && $tokens[$next]['code'] !== T_RETURN 213 ) { 214 $error = 'The call to the callback function must be followed by a return statement if it is not the last statement in the create() method'; 215 $phpcsFile->addError($error, $i, 'NoReturn'); 216 } 217 }//end for 218 219 if ($foundCallback === false) { 220 $error = 'The create() method of a widget type must call the callback function'; 221 $phpcsFile->addError($error, $create, 'CallbackNotCalled'); 222 } 223 224 }//end process() 225 226 227}//end class 228