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