• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 15253296250

26 May 2025 11:55AM UTC coverage: 78.632% (+0.3%) from 78.375%
15253296250

Pull #1105

github

web-flow
Merge d9441d98f into caf806050
Pull Request #1105: Skip tests when 'git' command is not available

19665 of 25009 relevant lines covered (78.63%)

88.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.9
/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php
1
<?php
2
/**
3
 * Ensures a file declares new symbols and causes no other side effects, or executes logic with side effects, but not both.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Standards\PSR1\Sniffs\Files;
11

12
use PHP_CodeSniffer\Files\File;
13
use PHP_CodeSniffer\Sniffs\Sniff;
14
use PHP_CodeSniffer\Util\Tokens;
15

16
class SideEffectsSniff implements Sniff
17
{
18

19

20
    /**
21
     * Returns an array of tokens this test wants to listen for.
22
     *
23
     * @return array<int|string>
24
     */
25
    public function register()
3✔
26
    {
27
        return [T_OPEN_TAG];
3✔
28

29
    }//end register()
30

31

32
    /**
33
     * Processes this sniff, when one of its tokens is encountered.
34
     *
35
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
36
     * @param int                         $stackPtr  The position of the current token in
37
     *                                               the token stack.
38
     *
39
     * @return int
40
     */
41
    public function process(File $phpcsFile, $stackPtr)
3✔
42
    {
43
        $tokens = $phpcsFile->getTokens();
3✔
44
        $result = $this->searchForConflict($phpcsFile, 0, ($phpcsFile->numTokens - 1), $tokens);
3✔
45

46
        if ($result['symbol'] !== null && $result['effect'] !== null) {
3✔
47
            $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.';
3✔
48
            $data  = [
2✔
49
                $tokens[$result['symbol']]['line'],
3✔
50
                $tokens[$result['effect']]['line'],
3✔
51
            ];
2✔
52
            $phpcsFile->addWarning($error, 0, 'FoundWithSymbols', $data);
3✔
53
            $phpcsFile->recordMetric($stackPtr, 'Declarations and side effects mixed', 'yes');
3✔
54
        } else {
55
            $phpcsFile->recordMetric($stackPtr, 'Declarations and side effects mixed', 'no');
3✔
56
        }
57

58
        // Ignore the rest of the file.
59
        return $phpcsFile->numTokens;
3✔
60

61
    }//end process()
62

63

64
    /**
65
     * Searches for symbol declarations and side effects.
66
     *
67
     * Returns the positions of both the first symbol declared and the first
68
     * side effect in the file. A NULL value for either indicates nothing was
69
     * found.
70
     *
71
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
72
     * @param int                         $start     The token to start searching from.
73
     * @param int                         $end       The token to search to.
74
     * @param array                       $tokens    The stack of tokens that make up
75
     *                                               the file.
76
     *
77
     * @return array
78
     */
79
    private function searchForConflict($phpcsFile, $start, $end, $tokens)
3✔
80
    {
81
        $symbols = [
2✔
82
            T_CLASS     => T_CLASS,
3✔
83
            T_INTERFACE => T_INTERFACE,
3✔
84
            T_TRAIT     => T_TRAIT,
3✔
85
            T_ENUM      => T_ENUM,
3✔
86
            T_FUNCTION  => T_FUNCTION,
3✔
87
        ];
2✔
88

89
        $conditions = [
2✔
90
            T_IF     => T_IF,
3✔
91
            T_ELSE   => T_ELSE,
3✔
92
            T_ELSEIF => T_ELSEIF,
3✔
93
        ];
2✔
94

95
        $checkAnnotations = $phpcsFile->config->annotations;
3✔
96

97
        $firstSymbol = null;
3✔
98
        $firstEffect = null;
3✔
99
        for ($i = $start; $i <= $end; $i++) {
3✔
100
            // Respect phpcs:disable comments.
101
            if ($checkAnnotations === true
3✔
102
                && $tokens[$i]['code'] === T_PHPCS_DISABLE
3✔
103
                && (empty($tokens[$i]['sniffCodes']) === true
3✔
104
                || isset($tokens[$i]['sniffCodes']['PSR1']) === true
3✔
105
                || isset($tokens[$i]['sniffCodes']['PSR1.Files']) === true
3✔
106
                || isset($tokens[$i]['sniffCodes']['PSR1.Files.SideEffects']) === true
3✔
107
                || isset($tokens[$i]['sniffCodes']['PSR1.Files.SideEffects.FoundWithSymbols']) === true)
3✔
108
            ) {
109
                do {
110
                    $i = $phpcsFile->findNext(T_PHPCS_ENABLE, ($i + 1));
3✔
111
                } while ($i !== false
3✔
112
                    && empty($tokens[$i]['sniffCodes']) === false
3✔
113
                    && isset($tokens[$i]['sniffCodes']['PSR1']) === false
3✔
114
                    && isset($tokens[$i]['sniffCodes']['PSR1.Files']) === false
3✔
115
                    && isset($tokens[$i]['sniffCodes']['PSR1.Files.SideEffects']) === false
3✔
116
                    && isset($tokens[$i]['sniffCodes']['PSR1.Files.SideEffects.FoundWithSymbols']) === false);
3✔
117

118
                if ($i === false) {
3✔
119
                    // The entire rest of the file is disabled,
120
                    // so return what we have so far.
121
                    break;
3✔
122
                }
123

124
                continue;
3✔
125
            }
126

127
            // Ignore whitespace and comments.
128
            if (isset(Tokens::EMPTY_TOKENS[$tokens[$i]['code']]) === true) {
3✔
129
                continue;
3✔
130
            }
131

132
            // Ignore PHP tags.
133
            if ($tokens[$i]['code'] === T_OPEN_TAG
3✔
134
                || $tokens[$i]['code'] === T_CLOSE_TAG
3✔
135
            ) {
136
                continue;
3✔
137
            }
138

139
            // Ignore shebang.
140
            if (substr($tokens[$i]['content'], 0, 2) === '#!') {
3✔
141
                continue;
3✔
142
            }
143

144
            // Ignore logical operators.
145
            if (isset(Tokens::BOOLEAN_OPERATORS[$tokens[$i]['code']]) === true) {
3✔
146
                continue;
3✔
147
            }
148

149
            // Ignore entire namespace, declare, const and use statements.
150
            if ($tokens[$i]['code'] === T_NAMESPACE
3✔
151
                || $tokens[$i]['code'] === T_USE
3✔
152
                || $tokens[$i]['code'] === T_DECLARE
3✔
153
                || $tokens[$i]['code'] === T_CONST
3✔
154
            ) {
155
                if (isset($tokens[$i]['scope_opener']) === true) {
3✔
156
                    $i = $tokens[$i]['scope_closer'];
3✔
157
                    if ($tokens[$i]['code'] === T_ENDDECLARE) {
3✔
158
                        $semicolon = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
3✔
159
                        if ($semicolon !== false && $tokens[$semicolon]['code'] === T_SEMICOLON) {
3✔
160
                            $i = $semicolon;
3✔
161
                        }
162
                    }
163
                } else {
164
                    $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($i + 1));
3✔
165
                    if ($semicolon !== false) {
3✔
166
                        $i = $semicolon;
3✔
167
                    }
168
                }
169

170
                continue;
3✔
171
            }
172

173
            // Ignore function/class prefixes.
174
            if (isset(Tokens::METHOD_MODIFIERS[$tokens[$i]['code']]) === true
3✔
175
                || $tokens[$i]['code'] === T_READONLY
3✔
176
            ) {
177
                continue;
3✔
178
            }
179

180
            // Ignore anon classes.
181
            if ($tokens[$i]['code'] === T_ANON_CLASS) {
3✔
182
                $i = $tokens[$i]['scope_closer'];
3✔
183
                continue;
3✔
184
            }
185

186
            // Ignore attributes.
187
            if ($tokens[$i]['code'] === T_ATTRIBUTE
3✔
188
                && isset($tokens[$i]['attribute_closer']) === true
3✔
189
            ) {
190
                $i = $tokens[$i]['attribute_closer'];
3✔
191
                continue;
3✔
192
            }
193

194
            // Detect and skip over symbols.
195
            if (isset($symbols[$tokens[$i]['code']]) === true
3✔
196
                && isset($tokens[$i]['scope_closer']) === true
3✔
197
            ) {
198
                if ($firstSymbol === null) {
3✔
199
                    $firstSymbol = $i;
3✔
200
                }
201

202
                $i = $tokens[$i]['scope_closer'];
3✔
203
                continue;
3✔
204
            } else if (($tokens[$i]['code'] === T_STRING
3✔
205
                || $tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED)
3✔
206
                && strtolower(ltrim($tokens[$i]['content'], '\\')) === 'define'
3✔
207
            ) {
208
                $prev = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($i - 1), null, true);
3✔
209
                if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR
3✔
210
                    && $tokens[$prev]['code'] !== T_NULLSAFE_OBJECT_OPERATOR
3✔
211
                    && $tokens[$prev]['code'] !== T_DOUBLE_COLON
3✔
212
                    && $tokens[$prev]['code'] !== T_FUNCTION
3✔
213
                ) {
214
                    if ($firstSymbol === null) {
3✔
215
                        $firstSymbol = $i;
3✔
216
                    }
217

218
                    $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($i + 1));
3✔
219
                    if ($semicolon !== false) {
3✔
220
                        $i = $semicolon;
3✔
221
                    }
222

223
                    continue;
3✔
224
                }
225
            }//end if
226

227
            // Special case for defined() as it can be used to see
228
            // if a constant (a symbol) should be defined or not and
229
            // doesn't need to use a full conditional block.
230
            if (($tokens[$i]['code'] === T_STRING
3✔
231
                || $tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED)
3✔
232
                && strtolower(ltrim($tokens[$i]['content'], '\\')) === 'defined'
3✔
233
            ) {
234
                $openBracket = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
3✔
235
                if ($openBracket !== false
3✔
236
                    && $tokens[$openBracket]['code'] === T_OPEN_PARENTHESIS
3✔
237
                    && isset($tokens[$openBracket]['parenthesis_closer']) === true
3✔
238
                ) {
239
                    $prev = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($i - 1), null, true);
3✔
240
                    if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR
3✔
241
                        && $tokens[$prev]['code'] !== T_NULLSAFE_OBJECT_OPERATOR
3✔
242
                        && $tokens[$prev]['code'] !== T_DOUBLE_COLON
3✔
243
                        && $tokens[$prev]['code'] !== T_FUNCTION
3✔
244
                    ) {
245
                        $i = $tokens[$openBracket]['parenthesis_closer'];
3✔
246
                        continue;
3✔
247
                    }
248
                }
249
            }//end if
250

251
            // Conditional statements are allowed in symbol files as long as the
252
            // contents is only a symbol definition. So don't count these as effects
253
            // in this case.
254
            if (isset($conditions[$tokens[$i]['code']]) === true) {
3✔
255
                if (isset($tokens[$i]['scope_opener']) === false) {
3✔
256
                    // Probably an "else if", so just ignore.
257
                    continue;
3✔
258
                }
259

260
                $result = $this->searchForConflict(
3✔
261
                    $phpcsFile,
3✔
262
                    ($tokens[$i]['scope_opener'] + 1),
3✔
263
                    ($tokens[$i]['scope_closer'] - 1),
3✔
264
                    $tokens
3✔
265
                );
2✔
266

267
                if ($result['symbol'] !== null) {
3✔
268
                    if ($firstSymbol === null) {
3✔
269
                        $firstSymbol = $result['symbol'];
×
270
                    }
271

272
                    if ($result['effect'] !== null) {
3✔
273
                        // Found a conflict.
274
                        $firstEffect = $result['effect'];
×
275
                        break;
×
276
                    }
277
                }
278

279
                if ($firstEffect === null) {
3✔
280
                    $firstEffect = $result['effect'];
3✔
281
                }
282

283
                $i = $tokens[$i]['scope_closer'];
3✔
284
                continue;
3✔
285
            }//end if
286

287
            if ($firstEffect === null) {
3✔
288
                $firstEffect = $i;
3✔
289
            }
290

291
            if ($firstSymbol !== null) {
3✔
292
                // We have a conflict we have to report, so no point continuing.
293
                break;
3✔
294
            }
295
        }//end for
296

297
        return [
2✔
298
            'symbol' => $firstSymbol,
3✔
299
            'effect' => $firstEffect,
3✔
300
        ];
2✔
301

302
    }//end searchForConflict()
303

304

305
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc