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